"""Deterministic balanced work sizing after semantic family review.""" from __future__ import annotations import math from collections import Counter from suite_contract import Problem, digest, validate_result from suite_policy import MAX_GROUP, POLICY_REVISION, projection def balanced_sizes(count): """Compute the minimum number of bounded parts without remainder singletons.""" if type(count) is not int or count < 1: raise ValueError("positive case count required") parts = math.ceil(count / MAX_GROUP) base, remainder = divmod(count, parts) return [base + 1] * remainder + [base] * (parts - remainder) def ordered_parts(group, by_alias): """Pack variation hints into fixed balanced sizes; equivalent text stays stable.""" def content(alias): return digest({k: v for k, v in by_alias[alias].items() if k != "alias"}) # Canonicalize every hint, not its model-generated order. Identical records use # the same hint owner even if the model placed their aliases in different hints. hints = [sorted(part, key=lambda a: (content(a), a)) for part in group["variation_sets"]] hints.sort(key=lambda part: tuple(sorted((content(a), a) for a in part))) owner = {} for index, part in enumerate(hints): for alias in part: owner.setdefault(content(alias), index) buckets = {} for alias in group["members"]: buckets.setdefault(owner[content(alias)], []).append(alias) blocks = [sorted(part, key=lambda a: (content(a), a)) for part in buckets.values()] blocks.sort(key=lambda part: (-len(part), tuple((content(a), a) for a in part))) sizes = balanced_sizes(len(group["members"])) parts = [[] for _ in sizes] for block in blocks: left = list(block) while left: room = [size - len(part) for size, part in zip(sizes, parts)] fits = [i for i, free in enumerate(room) if free >= len(left)] index = min(fits, key=lambda i: (room[i], i)) if fits else max(range(len(room)), key=lambda i: (room[i], -i)) count = min(room[index], len(left)) if count <= 0: raise Problem("invalid_capacity_partition", 502) parts[index].extend(left[:count]) left = left[count:] parts = [sorted(part) for part in parts] parts.sort(key=lambda part: (-len(part), tuple(part))) if [len(part) for part in parts] != sizes or Counter(a for p in parts for a in p) != Counter(group["members"]): raise Problem("invalid_capacity_partition", 502) return parts def cap_families(natural, request): """Create only final tasks, retaining conceptual-family sizing explanations.""" by_alias = {case["alias"]: case for case in request["cases"]} final, divisions = [], [] for group in sorted(natural["groups"], key=lambda g: (" ".join(g["name"].split()).casefold(), sorted(g["members"]))): base_name = " ".join(group["name"].split()) if len(group["members"]) <= MAX_GROUP: final.append({"name": base_name, "description": group["description"], "members": sorted(group["members"])}) continue parts = ordered_parts(group, by_alias) names = [] for index, members in enumerate(parts, 1): name = f"{base_name} ({index}/{len(parts)})" names.append(name) final.append({"name": name, "description": f"Work-size part {index}/{len(parts)} of one family. {group['common_work']}", "members": members}) divisions.append({"family_name": base_name, "natural_case_count": len(group["members"]), "part_sizes": [len(part) for part in parts], "part_names": names, "common_work": group["common_work"], "rationale": group["rationale"], "uncertainty": group["uncertainty"]}) result = validate_result({"groups": final}, request) # Recheck final names and memberships against each declared capacity division. by_name = {group["name"]: group for group in final} for division in divisions: if [len(by_name[name]["members"]) for name in division["part_names"]] != balanced_sizes(division["natural_case_count"]): raise Problem("invalid_capacity_partition", 502) return result, divisions def disagreements(a, b): """Compare co-membership sets, storing compressed partitions rather than O(n^2) pairs.""" def memberships(value): return {alias: frozenset(g["members"]) for g in value["groups"] for alias in g["members"]} first, second = memberships(a), memberships(b) changed = sorted(alias for alias in first if first[alias] != second[alias]) pair_count = sum(len(first[alias] ^ second[alias]) for alias in first) // 2 return {"aliases": changed, "pair_count": pair_count, "proposal_a": sorted(sorted(g["members"]) for g in a["groups"]), "proposal_b": sorted(sorted(g["members"]) for g in b["groups"])} def review_summary(a, b, reconciled, reviewed, audited, final, divisions): """Build an authorized result-only explanation, never routine operational metadata.""" natural = audited or reconciled return {"policy_revision": POLICY_REVISION, "proposal_disagreements": disagreements(a, b), "reconciled_families": reconciled["groups"], "large_family_review": reviewed.get("decisions", []) if reviewed else [], "decision_audit": audited.get("decisions", []) if audited else [], "natural_families": natural["groups"], "capacity_divisions": divisions, "unresolved_uncertainties": [{"family_name": g["name"], "members": sorted(g["members"]), "uncertainty": g["uncertainty"]} for g in natural["groups"] if g["uncertainty"].strip()], "counts": {"natural_families": len(natural["groups"]), "final_tasks": len(final["groups"]), "natural_singletons": sum(len(g["members"]) == 1 for g in natural["groups"]), "final_singletons": sum(len(g["members"]) == 1 for g in final["groups"])}, "sizing_is_not_semantic_evidence": True}