--- /SHiREVault/Backup/OSBackups/SHIRE-ACADEMY-ROUND3-AST-SOURCE-REPAIR-20260805T122334Z/before/brain_server.py 2026-08-05 20:23:33.690767800 +0800 +++ /SHiREVault/Backup/OSBackups/SHIRE-ACADEMY-ROUND3-AST-SOURCE-REPAIR-20260805T122334Z/staged/brain_server.py 2026-08-05 20:23:34.619040400 +0800 @@ -333,6 +333,52 @@ return clean +def _academy_canonicalise_round3_contract_fields(payload, exam_round): + """ + Repair only the safe Round 3 field-placement error observed in production. + + Round 3 acceptance criteria belong inside transfer_project. A root-level + value is moved only when the nested value is absent. An identical duplicate + is pruned. Conflicting values remain a hard contract failure. + """ + if int(exam_round) != 3 or not isinstance(payload, dict): + return payload, [] + + if "acceptance_criteria" not in payload: + return payload, [] + + normalised = dict(payload) + project_value = normalised.get("transfer_project") + + if not isinstance(project_value, dict): + return normalised, [] + + project = dict(project_value) + root_criteria = normalised.get("acceptance_criteria") + nested_criteria = project.get("acceptance_criteria") + + if nested_criteria is None: + project["acceptance_criteria"] = root_criteria + normalised["transfer_project"] = project + del normalised["acceptance_criteria"] + + return normalised, [ + "moved_root_acceptance_criteria_to_transfer_project" + ] + + if nested_criteria == root_criteria: + del normalised["acceptance_criteria"] + + return normalised, [ + "pruned_duplicate_root_acceptance_criteria" + ] + + raise AcademyStructuredOutputError( + "Academy structured output invalid: conflicting acceptance_criteria " + "at the Round 3 root and inside transfer_project" + ) + + def _academy_validate_shape(payload, exam_round): exam_round = int(exam_round) required = ACADEMY_ROUND_REQUIRED_KEYS.get(exam_round) @@ -492,19 +538,157 @@ ) elif exam_round == 3: project = payload.get("transfer_project") + if not isinstance(project, dict): raise AcademyStructuredOutputError( "Academy structured output invalid: round 3 transfer project missing" ) - if not isinstance(project.get("steps"), list) or len(project["steps"]) != 3: + + required_project = { + "title", + "goal", + "inputs", + "steps", + "outputs", + "acceptance_criteria", + } + + missing_project = sorted( + required_project - set(project) + ) + extra_project = sorted( + set(project) - required_project + ) + + if missing_project: + raise AcademyStructuredOutputError( + "Academy structured output invalid: transfer_project missing: " + + ", ".join(missing_project) + ) + + if extra_project: + raise AcademyStructuredOutputError( + "Academy structured output invalid: transfer_project " + "unexpected fields: " + ", ".join(extra_project) + ) + + if len(str(project.get("title") or "").strip()) < 8: + raise AcademyStructuredOutputError( + "Academy structured output invalid: round 3 project title is too short" + ) + + if len(str(project.get("goal") or "").strip()) < 20: + raise AcademyStructuredOutputError( + "Academy structured output invalid: round 3 project goal is too short" + ) + + inputs = project.get("inputs") + + if ( + not isinstance(inputs, list) + or not (1 <= len(inputs) <= 3) + or not all( + isinstance(item, str) + and len(item.strip()) >= 8 + for item in inputs + ) + ): + raise AcademyStructuredOutputError( + "Academy structured output invalid: round 3 needs one to " + "three meaningful project inputs" + ) + + steps = project.get("steps") + + if ( + not isinstance(steps, list) + or len(steps) != 3 + or not all( + isinstance(item, str) + and len(item.strip()) >= 8 + for item in steps + ) + ): + raise AcademyStructuredOutputError( + "Academy structured output invalid: round 3 needs exactly " + "three meaningful project steps" + ) + + outputs = project.get("outputs") + + if ( + not isinstance(outputs, list) + or not (1 <= len(outputs) <= 2) + or not all( + isinstance(item, str) + and len(item.strip()) >= 8 + for item in outputs + ) + ): + raise AcademyStructuredOutputError( + "Academy structured output invalid: round 3 needs one or " + "two meaningful project outputs" + ) + + criteria = project.get("acceptance_criteria") + + if ( + not isinstance(criteria, list) + or len(criteria) != 2 + or not all( + isinstance(item, str) + and len(item.strip()) >= 8 + for item in criteria + ) + ): raise AcademyStructuredOutputError( - "Academy structured output invalid: round 3 needs three project steps" + "Academy structured output invalid: round 3 needs exactly " + "two measurable acceptance criteria inside transfer_project" ) - if not isinstance(payload.get("boundary_cases"), dict): + + boundary_cases = payload.get("boundary_cases") + + if not isinstance(boundary_cases, dict): raise AcademyStructuredOutputError( "Academy structured output invalid: round 3 boundary cases missing" ) + required_boundaries = { + "minimum", + "nominal", + "maximum", + "invalid", + } + + missing_boundaries = sorted( + required_boundaries - set(boundary_cases) + ) + extra_boundaries = sorted( + set(boundary_cases) - required_boundaries + ) + + if missing_boundaries: + raise AcademyStructuredOutputError( + "Academy structured output invalid: boundary_cases missing: " + + ", ".join(missing_boundaries) + ) + + if extra_boundaries: + raise AcademyStructuredOutputError( + "Academy structured output invalid: boundary_cases " + "unexpected fields: " + ", ".join(extra_boundaries) + ) + + if not all( + isinstance(boundary_cases[name], str) + and len(boundary_cases[name].strip()) >= 8 + for name in required_boundaries + ): + raise AcademyStructuredOutputError( + "Academy structured output invalid: every Round 3 boundary " + "case must be a meaningful plain-language string" + ) + return {key: payload[key] for key in allowed if key in payload} @@ -566,18 +750,33 @@ for method, value in attempts: try: parsed_payload = json.loads(value) - payload = _academy_validate_shape(parsed_payload, exam_round) + parsed_payload, contract_notes = ( + _academy_canonicalise_round3_contract_fields( + parsed_payload, + exam_round, + ) + ) + payload = _academy_validate_shape( + parsed_payload, + exam_round, + ) + normalisation_method = ( + method + if not contract_notes + else method + "+round3_contract_canonicalisation" + ) return { "payload": payload, "metadata_pruned": sorted(set(parsed_payload) - set(payload)), + "canonicalisation_notes": contract_notes, "canonical": json.dumps( payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True, ), - "repaired": method != "strict_json", - "method": method, + "repaired": method != "strict_json" or bool(contract_notes), + "method": normalisation_method, "raw_sha256": hashlib.sha256( raw.encode("utf-8", errors="replace") ).hexdigest(), @@ -590,10 +789,26 @@ python_candidate = re.sub(r"\bnull\b", "None", python_candidate, flags=re.IGNORECASE) try: parsed_payload = ast.literal_eval(python_candidate) - payload = _academy_validate_shape(parsed_payload, exam_round) + parsed_payload, contract_notes = ( + _academy_canonicalise_round3_contract_fields( + parsed_payload, + exam_round, + ) + ) + payload = _academy_validate_shape( + parsed_payload, + exam_round, + ) + normalisation_method = ( + "python_literal_repair" + if not contract_notes + else + "python_literal_repair+round3_contract_canonicalisation" + ) return { "payload": payload, "metadata_pruned": sorted(set(parsed_payload) - set(payload)), + "canonicalisation_notes": contract_notes, "canonical": json.dumps( payload, ensure_ascii=False, @@ -601,7 +816,7 @@ sort_keys=True, ), "repaired": True, - "method": "python_literal_repair", + "method": normalisation_method, "raw_sha256": hashlib.sha256( raw.encode("utf-8", errors="replace") ).hexdigest(), @@ -1345,8 +1560,11 @@ { "ok": False, "error": str(exc), - "retryable": True, - "infrastructure_failure": True, + "retryable": False, + "infrastructure_failure": False, + "content_format_failure": True, + "failure_class": + "academy_output_contract_failure", "output_contract": ACADEMY_OUTPUT_CONTRACT, "exam_round": academy_exam_round, "raw_answer": academy_raw_answer, @@ -1363,7 +1581,7 @@ ) ).hexdigest() if academy_initial_raw_answer else "", }, - status=502, + status=422, ) except ValueError as exc: json_response(self, {"ok": False, "error": str(exc)}, status=400)