--- /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)
