atlas-iac/services/hermes/scripts/suite_sizing.py

115 lines
6.1 KiB
Python
Raw Normal View History

"""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}