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

209 lines
12 KiB
Python

"""Internal multi-pass schemas, prompts, and validation; no client fields added."""
from __future__ import annotations
import copy
import re
from collections import Counter
from suite_contract import FIELDS, Problem, SCHEMA, SYSTEM, encoded, validate_partition
from suite_assignments import WIRE_INSTRUCTIONS, decision_keys, wire_schema
POLICY_REVISION = "implementation-five-v1-20260929"
MAX_GROUP = 5
BASE_NAME_LIMIT = 56 # Leaves room for ' (80/80)' at the 400-case input limit.
STAGES = ("proposal_a", "proposal_b", "reconciliation", "large_family_review", "decision_audit")
DISCOVERY = (
"Discover natural implementation families across this COMPLETE suite, without any "
"task-size cap. Do not split a coherent family for a numerical quota. Names normally "
"use two to five words describing test machinery; never repeat the hierarchy. "
"Use distinct meaningful mechanism names, not numbering to hide collisions. "
"Use StructuredOutput directly when available; do not first emit a prose draft."
)
RECONCILE = (
"Independently reassess two complete candidate partitions using ALL original cases. "
"Compare memberships, never proposal names or group numbers. Resolve disagreements "
"against success_criteria and shared test machinery. Do not vote, count majority "
"co-memberships, or take transitive connected components. Agreement is not proof. "
"Return one coherent NATURAL partition, with no size cap. Review cross-family merges "
"and unjustified splits. Record concise engineering rationales and unresolved "
"uncertainty, not private reasoning traces. Evidence must quote a short exact substring "
"from a named source field of an assigned alias. Prefer success_criteria when present. "
"common_work must describe ONLY machinery applicable to EVERY assigned member, "
"without enumerating objectives/variations that apply to only some members. "
"variation_sets partition each family's aliases into closely related variations; "
"these are ordering hints, not additional task levels or semantic families. "
"Keep identical case content together in variation_sets. Never impose five-member "
"limits on natural discovery. Use unique mechanism names, normally two to five words "
"and at most 56 characters, without numbered suffixes. Rewrite long/colliding names "
"meaningfully; do not blindly truncate or add arbitrary identifiers. "
"Use StructuredOutput directly when available; no prose draft."
)
REVIEW = (
"Review EVERY listed oversized family against its original source case fields, "
"especially success_criteria. Decide whether different control/measurement/evidence "
"mechanisms warrant separate natural families with meaningful distinct names. "
"Do not split by source order, quota, nominal/fault labels alone, or cheap assertion "
"variations. If coherent, explicitly explain the common machinery and why differences "
"are inexpensive variations. Preserve that coherent natural family even if it has "
"more than five members: deterministic work sizing happens later. Return a complete "
"natural partition of the whole suite and one explicit decision per reviewed family. "
"Decisions refer to the exact reviewed source_members, not names. Splits must partition "
"that family's members; do not cross existing family boundaries during this review. "
"Retain and improve concise source-field support and uncertainty."
)
AUDIT = (
"Perform ONE bounded additional review of the previous large-family decisions. "
"Using all original fields, challenge unjustified semantic splits, unjustified broad "
"merges, and assertions that would need different machinery. You may correct the "
"decision by merging/splitting only within each ORIGINAL oversized family. Re-examine "
"all its descendants even if each is now small. Return the complete final NATURAL "
"partition and final decisions for every original oversized family. There will be "
"no loop seeking agreement; explicitly retain unresolved uncertainty. Preserve "
"coherent large families for deterministic capacity division afterwards."
)
def text_schema(limit):
"""Return a bounded string schema for internal model output."""
return {"type": "string", "minLength": 1, "maxLength": limit}
ALIAS_LIST = {"type": "array", "minItems": 1, "items": text_schema(53)}
EVIDENCE = {"type": "array", "minItems": 1, "maxItems": 3, "items": {
"type": "object", "additionalProperties": False,
"required": ["alias", "field", "quote"], "properties": {
"alias": text_schema(53), "field": {"type": "string", "enum": sorted(FIELDS)},
"quote": text_schema(160)}}}
DISCOVERY_SCHEMA = copy.deepcopy(SCHEMA)
DISCOVERY_SCHEMA["properties"]["groups"]["items"]["properties"]["members"].pop("maxItems", None)
DISCOVERY_SCHEMA["properties"]["groups"]["items"]["properties"]["name"]["maxLength"] = BASE_NAME_LIMIT
NATURAL_SCHEMA = copy.deepcopy(DISCOVERY_SCHEMA)
GROUP_PROPERTIES = NATURAL_SCHEMA["properties"]["groups"]["items"]["properties"]
GROUP_PROPERTIES["name"]["maxLength"] = BASE_NAME_LIMIT
GROUP_PROPERTIES.update({
"common_work": text_schema(160), "rationale": text_schema(320),
"uncertainty": {"type": "string", "maxLength": 240}, "evidence": EVIDENCE,
"variation_sets": {"type": "array", "minItems": 1, "items": ALIAS_LIST},
})
NATURAL_SCHEMA["properties"]["groups"]["items"]["required"] = list(GROUP_PROPERTIES)
REVIEW_SCHEMA = copy.deepcopy(NATURAL_SCHEMA)
REVIEW_SCHEMA["required"].append("decisions")
REVIEW_SCHEMA["properties"]["decisions"] = {"type": "array", "minItems": 1, "items": {
"type": "object", "additionalProperties": False,
"required": ["source_members", "decision", "rationale", "evidence"], "properties": {
"source_members": ALIAS_LIST, "decision": {"type": "string", "enum": ["keep", "split"]},
"rationale": text_schema(320), "evidence": EVIDENCE}}}
def invocation(stage, request, context=None):
"""Build one full-input pass; independent discovery has no proposal context."""
if stage not in STAGES:
raise ValueError("unknown internal stage")
discovery = stage in STAGES[:2]
if discovery and context:
raise ValueError("discovery must be independent")
schema = DISCOVERY_SCHEMA if discovery else NATURAL_SCHEMA if stage == "reconciliation" else REVIEW_SCHEMA
instruction = DISCOVERY if discovery else RECONCILE
if stage == "large_family_review":
instruction += "\n" + REVIEW
if stage == "decision_audit":
instruction += "\n" + REVIEW + "\n" + AUDIT
source = {k: request[k] for k in ("campaign", "suite", "cases")}
context = dict(context or {})
keys = decision_keys(context) if stage in STAGES[3:] else {}
if keys:
context["review_keys"] = keys
return {"stage": stage,
"schema": wire_schema(schema, request, context), "review_keys": keys,
"input": encoded({"suite": source, "review_material": context}).decode(),
"system": SYSTEM + "\n\nPASS INSTRUCTIONS:\n" + instruction + "\n\n" + WIRE_INSTRUCTIONS}
def projection(value):
"""Extract only the unchanged public group shape from a natural partition."""
return {"groups": [{k: group[k] for k in ("name", "description", "members")}
for group in value["groups"]]}
def bounded_string(value, limit, *, empty=False):
"""Reject invalid internal strings without echoing their contents."""
if type(value) is not str or len(value) > limit or (not empty and not value.strip()):
raise Problem("invalid_review_output", 502)
def evidence(value, members, by_alias):
"""Require short exact source-field support within the explained membership."""
if not isinstance(value, list) or not 1 <= len(value) <= 3:
raise Problem("invalid_review_evidence", 502)
for item in value:
if not isinstance(item, dict) or set(item) != {"alias", "field", "quote"}:
raise Problem("invalid_review_evidence", 502)
alias, field, quote = item["alias"], item["field"], item["quote"]
if type(alias) is not str or alias not in members or type(field) is not str or field not in FIELDS:
raise Problem("invalid_review_evidence", 502)
bounded_string(quote, 160)
source = by_alias[alias].get(field)
if not isinstance(source, str) or quote not in source:
raise Problem("invalid_review_evidence", 502)
def validate_natural(value, request, originals=None):
"""Validate complete natural assignments and bounded explanations, without a cap."""
expected_keys = {"groups", "decisions"} if originals is not None else {"groups"}
if not isinstance(value, dict) or set(value) != expected_keys or not isinstance(value["groups"], list):
raise Problem("invalid_review_output", 502)
required = set(GROUP_PROPERTIES)
for group in value["groups"]:
if not isinstance(group, dict) or set(group) != required:
raise Problem("invalid_review_output", 502)
validate_partition(projection(value), request, name_limit=BASE_NAME_LIMIT)
by_alias = {case["alias"]: case for case in request["cases"]}
for group in value["groups"]:
members = set(group["members"])
for field, limit in (("common_work", 160), ("rationale", 320), ("uncertainty", 240)):
bounded_string(group[field], limit, empty=field == "uncertainty")
if re.search(r"\(\s*\d+\s*/\s*\d+\s*\)$", group["name"]):
raise Problem("invalid_natural_family_name", 502)
evidence(group["evidence"], members, by_alias)
variations = group["variation_sets"]
if not isinstance(variations, list) or not variations or any(
not isinstance(part, list) or not part or any(type(a) is not str for a in part)
for part in variations):
raise Problem("invalid_variation_assignments", 502)
if Counter(a for part in variations for a in part) != Counter(group["members"]):
raise Problem("invalid_variation_assignments", 502)
if originals is not None:
validate_decisions(value, originals, by_alias)
return value
def validate_decisions(value, originals, by_alias):
"""Audit every original oversized family and forbid cross-family review drift."""
decisions = value["decisions"]
large = {frozenset(g["members"]): g for g in originals if len(g["members"]) > MAX_GROUP}
if not isinstance(decisions, list) or len(decisions) != len(large):
raise Problem("incomplete_large_family_review", 502)
seen = set()
for decision in decisions:
if not isinstance(decision, dict) or set(decision) != {"source_members", "decision", "rationale", "evidence"}:
raise Problem("invalid_review_output", 502)
members = decision["source_members"]
if not isinstance(members, list) or any(type(a) is not str for a in members):
raise Problem("invalid_review_output", 502)
key = frozenset(members)
if key not in large or key in seen or len(key) != len(members):
raise Problem("incomplete_large_family_review", 502)
seen.add(key)
descendants = [g for g in value["groups"] if set(g["members"]) <= key]
split = len(descendants) > 1
if not descendants or decision["decision"] != ("split" if split else "keep"):
raise Problem("invalid_review_decision", 502)
bounded_string(decision["rationale"], 320)
evidence(decision["evidence"], key, by_alias)
for group in value["groups"]:
parents = [g for g in originals if set(group["members"]) <= set(g["members"])]
if len(parents) != 1 or (len(parents[0]["members"]) <= MAX_GROUP and
set(group["members"]) != set(parents[0]["members"])):
raise Problem("review_crossed_family_boundary", 502)