"""Score exhaustive synthetic Career Profile annotations; never substitutes for model qualification.""" import argparse from collections import Counter import json from pathlib import Path import unicodedata def normalise(value): return " ".join(unicodedata.normalize("NFC", str(value)).split()).casefold() def facts(profile): """Keep record associations and repeated claims, ignoring implementation-only metadata.""" result = Counter() def walk(value, path): if isinstance(value, dict): for key, item in value.items(): if key not in {"metadata", "version", "sections", "id", "startDate", "endDate", "dateNormalized"}: walk(item, f"{path}.{key}" if path else key) elif isinstance(value, list): for item in value: record = "" if isinstance(item, dict): identity = [key for key in ("company", "title", "institution", "qualification", "name", "start", "end", "date") if item.get(key)] record = "[" + "|".join(f"{key}={normalise(item[key])}" for key in identity) + "]" walk(item, path + record) elif value not in (None, "", False): result[(path, normalise(value))] += 1 walk(profile, "") return result def ratio(numerator, denominator): return numerator / denominator if denominator else 1.0 def evaluate_case(expected, actual): truth, predicted = facts(expected), facts(actual) correct = sum((truth & predicted).values()) date_truth = Counter({key: count for key, count in truth.items() if key[0].rsplit(".", 1)[-1] in {"start", "end", "date", "isCurrent"}}) date_predicted = Counter({key: count for key, count in predicted.items() if key[0].rsplit(".", 1)[-1] in {"start", "end", "date", "isCurrent"}}) category_equal = lambda category: facts({category: expected.get(category, [])}) == facts({category: actual.get(category, [])}) return { "truePositive": correct, "falsePositive": sum((predicted - truth).values()), "falseNegative": sum((truth - predicted).values()), "precision": ratio(correct, predicted.total()), "recall": ratio(correct, truth.total()), "datePrecision": ratio((date_truth & date_predicted).total(), date_predicted.total()), "dateRecall": ratio((date_truth & date_predicted).total(), date_truth.total()), "employmentExact": category_equal("jobs"), "educationExact": category_equal("education"), "skillsExact": category_equal("skills"), "unsupportedClaims": [{"path": path, "value": value, "count": count} for (path, value), count in (predicted - truth).items()], "missingClaims": [{"path": path, "value": value, "count": count} for (path, value), count in (truth - predicted).items()], } def evaluate_run(corpus, outputs): ids = [case["id"] for case in corpus] if len(set(ids)) != len(ids) or set(outputs) - set(ids): raise ValueError("Duplicate annotations or unexpected output cases.") cases = [] for case in corpus: output = outputs.get(case["id"], {}) result = evaluate_case(case["expected"], output.get("profile", {})) result.update(id=case["id"], completed="profile" in output, split=case.get("split", "development"), extractionMethod=output.get("extractionMethod", "NOT RUN"), timing=output.get("timing", {}), reviewRequired=output.get("reviewRequired", "NOT MEASURED")) cases.append(result) tp = sum(case["truePositive"] for case in cases) fp = sum(case["falsePositive"] for case in cases) fn = sum(case["falseNegative"] for case in cases) return {"cases": cases, "caseCount": len(cases), "completion": ratio(sum(c["completed"] for c in cases), len(cases)), "microPrecision": ratio(tp, tp + fp), "microRecall": ratio(tp, tp + fn), "unsupportedClaimCount": fp, "missingClaimCount": fn, "employmentExactCases": sum(c["employmentExact"] and c["completed"] for c in cases), "educationExactCases": sum(c["educationExact"] and c["completed"] for c in cases), "realAiAccuracy": "BLOCKED", "mockedAiCountsAsModelAccuracy": False, "limits": "Synthetic exhaustive annotations only. Unsupported claims include association errors, not necessarily invented text. No population accuracy estimate, human acceptance or model inference is implied."} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--corpus", type=Path, default=Path(__file__).with_name("corpus.json")) parser.add_argument("--actual", required=True, type=Path, help="JSON object keyed by case ID, each with a canonical profile.") parser.add_argument("--output", required=True, type=Path) parser.add_argument("--split", choices=("development", "frozen")) args = parser.parse_args() corpus = json.loads(args.corpus.read_text(encoding="utf-8")) outputs = json.loads(args.actual.read_text(encoding="utf-8")) if args.split: corpus = [case for case in corpus if case["split"] == args.split] outputs = {case["id"]: outputs[case["id"]] for case in corpus if case["id"] in outputs} report = evaluate_run(corpus, outputs) args.output.parent.mkdir(parents=True, exist_ok=True) with args.output.open("x", encoding="utf-8") as target: json.dump(report, target, indent=2, ensure_ascii=False) print(json.dumps({key: value for key, value in report.items() if key != "cases"}, indent=2)) if __name__ == "__main__": main()