A5-R3-R7-R1 English section structural ownership + seal continuity

#116
Files changed (1) hide show
  1. core/generation_runner.py +61 -0
core/generation_runner.py CHANGED
@@ -917,6 +917,52 @@ class GenerationRunner:
917
  return ''
918
  return f"# {title}\n" + match.group(1).strip()
919
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  @staticmethod
921
  def _english_prefix_through_top_level(manuscript: str, title: str) -> str:
922
  value = str(manuscript or '')
@@ -4629,6 +4675,21 @@ The previous attempt for this same manuscript unit was rejected by validation. R
4629
  state.pop("section_retry_exhausted", None)
4630
  break
4631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4632
  # Recompute citation survival/claim demand from the exact text that
4633
  # will be sealed, not from an earlier pre-cleanup intermediate.
4634
  if not is_quarantined and section_id in self._citation_eligible_section_ids(mode):
 
917
  return ''
918
  return f"# {title}\n" + match.group(1).strip()
919
 
920
+ @staticmethod
921
+ def _normalize_orchestrator_owned_headings(text: str, task: dict) -> str:
922
+ """Restore immutable H1/H2 structure after any model-owned repair path.
923
+
924
+ Writer and repair models own prose only. The orchestrator owns the
925
+ expected top-level and subsection headings described by the generation
926
+ task. This deterministic normalizer removes only exact markdown
927
+ heading lines for those owned titles, then restores the canonical
928
+ heading(s) required for this unit. All non-heading text is preserved.
929
+
930
+ The operation is intentionally idempotent: N(N(x)) == N(x).
931
+ """
932
+ value = str(text or "")
933
+ task = dict(task or {})
934
+ section_title = str(task.get("section") or "").strip()
935
+ subsection_title = str(task.get("subsection") or "").strip()
936
+ section_first = bool(task.get("section_first")) and bool(section_title)
937
+ subsection_first = bool(task.get("subsection_first")) and bool(subsection_title)
938
+
939
+ owned_titles = [title for title in (section_title, subsection_title) if title]
940
+ if not owned_titles:
941
+ return value
942
+
943
+ patterns = [
944
+ re.compile(rf"^[ \\t]*#{{1,6}}[ \\t]+{re.escape(title)}[ \\t]*$")
945
+ for title in owned_titles
946
+ ]
947
+ kept_lines = []
948
+ for line in value.splitlines(keepends=True):
949
+ visible = line.rstrip("\r\n")
950
+ if any(pattern.fullmatch(visible) for pattern in patterns):
951
+ continue
952
+ kept_lines.append(line)
953
+ body = "".join(kept_lines).lstrip("\r\n")
954
+
955
+ canonical = []
956
+ if section_first:
957
+ canonical.append(f"# {section_title}")
958
+ if subsection_first:
959
+ canonical.append(f"## {subsection_title}")
960
+ if not canonical:
961
+ return body
962
+
963
+ prefix = "\n\n".join(canonical)
964
+ return prefix if not body else prefix + "\n\n" + body
965
+
966
  @staticmethod
967
  def _english_prefix_through_top_level(manuscript: str, title: str) -> str:
968
  value = str(manuscript or '')
 
4675
  state.pop("section_retry_exhausted", None)
4676
  break
4677
 
4678
+ # R7-R1: heading structure is orchestrator-owned, not model-owned.
4679
+ # Any adaptive repair may replace the accepted prose bytes and drop
4680
+ # or duplicate the system-inserted H1/H2. Restore only those exact
4681
+ # structural anchors after the final model-owned repair and before
4682
+ # the unit is appended to the immutable English source / Section Seal.
4683
+ normalized_en_resolved = self._normalize_orchestrator_owned_headings(en_resolved, sec)
4684
+ if normalized_en_resolved != en_resolved:
4685
+ logging.info("Deterministic English heading normalization | unit=%s", display_name)
4686
+ quality_diagnostics.append({
4687
+ "scope": "english_structural_ownership",
4688
+ "name": display_name,
4689
+ "status": "deterministic_heading_normalization",
4690
+ })
4691
+ en_resolved = normalized_en_resolved
4692
+
4693
  # Recompute citation survival/claim demand from the exact text that
4694
  # will be sealed, not from an earlier pre-cleanup intermediate.
4695
  if not is_quarantined and section_id in self._citation_eligible_section_ids(mode):