Spaces:
Running
Running
S0-R1-C5.5-R6-R3 Cross-Mode Four-Language Repair Entitlement Closure
#91
by GuXiaopeng - opened
- core/generation_runner.py +54 -19
- core/pipeline_agent.py +42 -6
core/generation_runner.py
CHANGED
|
@@ -1082,7 +1082,10 @@ class GenerationRunner:
|
|
| 1082 |
lineages = {cls._section_violation_lineage(row) for row in (rows or [])}
|
| 1083 |
exhausted = {name for name in lineages if int(lineage_attempts.get(name, 0)) >= 1}
|
| 1084 |
fresh = lineages - exhausted
|
| 1085 |
-
|
|
|
|
|
|
|
|
|
|
| 1086 |
|
| 1087 |
@classmethod
|
| 1088 |
def _partition_section_violations(cls, rows, *, section_name: str = "", mode: str = "", exhausted: bool = False):
|
|
@@ -1097,6 +1100,30 @@ class GenerationRunner:
|
|
| 1097 |
blocking.append(row)
|
| 1098 |
return blocking, advisory
|
| 1099 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
@staticmethod
|
| 1101 |
def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None:
|
| 1102 |
"""Append reader-facing, nonfatal notes without exposing control vocabulary."""
|
|
@@ -3920,23 +3947,27 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3920 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 3921 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 3922 |
)
|
| 3923 |
-
|
| 3924 |
-
|
| 3925 |
-
|
| 3926 |
-
|
| 3927 |
-
|
| 3928 |
for lineage in fresh_lineages:
|
| 3929 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 3930 |
-
attempt_feedback = self._section_retry_feedback(
|
| 3931 |
logging.warning(
|
| 3932 |
-
"Unified Phase-1 Section lineage retry | unit=%s attempt=%s lineages=%s count=%s types=%s",
|
| 3933 |
-
display_name, section_attempt, sorted(fresh_lineages), len(
|
| 3934 |
-
sorted({str(row.get("type") or "") for row in
|
| 3935 |
)
|
| 3936 |
self._restore_section_citation_state(
|
| 3937 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3938 |
)
|
| 3939 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3940 |
if advisory_rows:
|
| 3941 |
self._record_section_advisories(
|
| 3942 |
quality_notes, advisory_rows, section_name=display_name
|
|
@@ -4104,23 +4135,27 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 4104 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 4105 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 4106 |
)
|
| 4107 |
-
|
| 4108 |
-
|
| 4109 |
-
|
| 4110 |
-
|
| 4111 |
-
|
| 4112 |
for lineage in fresh_lineages:
|
| 4113 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 4114 |
-
attempt_feedback = self._section_retry_feedback(
|
| 4115 |
logging.warning(
|
| 4116 |
-
"Post-normalization Section lineage retry | unit=%s attempt=%s lineages=%s count=%s types=%s",
|
| 4117 |
-
display_name, section_attempt, sorted(fresh_lineages), len(
|
| 4118 |
-
sorted({str(row.get("type") or "") for row in
|
| 4119 |
)
|
| 4120 |
self._restore_section_citation_state(
|
| 4121 |
agent, citation_plan, display_name, frozen_citation_state
|
| 4122 |
)
|
| 4123 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4124 |
if advisory_rows:
|
| 4125 |
self._record_section_advisories(
|
| 4126 |
quality_notes, advisory_rows, section_name=display_name
|
|
|
|
| 1082 |
lineages = {cls._section_violation_lineage(row) for row in (rows or [])}
|
| 1083 |
exhausted = {name for name in lineages if int(lineage_attempts.get(name, 0)) >= 1}
|
| 1084 |
fresh = lineages - exhausted
|
| 1085 |
+
# R6-R3 fairness closure: an exhausted lineage may not steal the bounded
|
| 1086 |
+
# repair entitlement of a genuinely fresh lineage discovered in the same
|
| 1087 |
+
# validation round. The Section hard cap remains the global stop.
|
| 1088 |
+
return bool(fresh) and not hard_cap_reached, exhausted, fresh
|
| 1089 |
|
| 1090 |
@classmethod
|
| 1091 |
def _partition_section_violations(cls, rows, *, section_name: str = "", mode: str = "", exhausted: bool = False):
|
|
|
|
| 1100 |
blocking.append(row)
|
| 1101 |
return blocking, advisory
|
| 1102 |
|
| 1103 |
+
@classmethod
|
| 1104 |
+
def _partition_section_violations_by_lineage(
|
| 1105 |
+
cls, rows, lineage_attempts, *, section_name: str = "", mode: str = "",
|
| 1106 |
+
hard_cap_reached: bool = False,
|
| 1107 |
+
):
|
| 1108 |
+
"""Classify each defect using only its own repair-exhaustion state.
|
| 1109 |
+
|
| 1110 |
+
R6-R3 prevents a previously exhausted sibling lineage from globally
|
| 1111 |
+
changing the severity of a newly discovered defect. When the Section
|
| 1112 |
+
hard cap is reached every lineage is evaluated as exhausted.
|
| 1113 |
+
"""
|
| 1114 |
+
blocking, advisory = [], []
|
| 1115 |
+
for row in list(rows or []):
|
| 1116 |
+
lineage = cls._section_violation_lineage(row)
|
| 1117 |
+
lineage_exhausted = bool(hard_cap_reached) or int(lineage_attempts.get(lineage, 0)) >= 1
|
| 1118 |
+
severity = cls._section_violation_severity(
|
| 1119 |
+
row, section_name=section_name, mode=mode, exhausted=lineage_exhausted
|
| 1120 |
+
)
|
| 1121 |
+
if severity == "ADVISORY":
|
| 1122 |
+
advisory.append(row)
|
| 1123 |
+
else:
|
| 1124 |
+
blocking.append(row)
|
| 1125 |
+
return blocking, advisory
|
| 1126 |
+
|
| 1127 |
@staticmethod
|
| 1128 |
def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None:
|
| 1129 |
"""Append reader-facing, nonfatal notes without exposing control vocabulary."""
|
|
|
|
| 3947 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 3948 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 3949 |
)
|
| 3950 |
+
if may_retry:
|
| 3951 |
+
retry_rows = [
|
| 3952 |
+
row for row in preview_blocking
|
| 3953 |
+
if self._section_violation_lineage(row) in fresh_lineages
|
| 3954 |
+
]
|
| 3955 |
for lineage in fresh_lineages:
|
| 3956 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 3957 |
+
attempt_feedback = self._section_retry_feedback(retry_rows)
|
| 3958 |
logging.warning(
|
| 3959 |
+
"Unified Phase-1 Section lineage retry | unit=%s attempt=%s lineages=%s exhausted_siblings=%s count=%s types=%s",
|
| 3960 |
+
display_name, section_attempt, sorted(fresh_lineages), sorted(exhausted_lineages), len(retry_rows),
|
| 3961 |
+
sorted({str(row.get("type") or "") for row in retry_rows if isinstance(row, dict)}),
|
| 3962 |
)
|
| 3963 |
self._restore_section_citation_state(
|
| 3964 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3965 |
)
|
| 3966 |
continue
|
| 3967 |
+
blocking_rows, advisory_rows = self._partition_section_violations_by_lineage(
|
| 3968 |
+
phase1_validation_rows, lineage_attempts, section_name=display_name, mode=mode,
|
| 3969 |
+
hard_cap_reached=hard_cap_reached
|
| 3970 |
+
)
|
| 3971 |
if advisory_rows:
|
| 3972 |
self._record_section_advisories(
|
| 3973 |
quality_notes, advisory_rows, section_name=display_name
|
|
|
|
| 4135 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 4136 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 4137 |
)
|
| 4138 |
+
if may_retry:
|
| 4139 |
+
retry_rows = [
|
| 4140 |
+
row for row in preview_blocking
|
| 4141 |
+
if self._section_violation_lineage(row) in fresh_lineages
|
| 4142 |
+
]
|
| 4143 |
for lineage in fresh_lineages:
|
| 4144 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 4145 |
+
attempt_feedback = self._section_retry_feedback(retry_rows)
|
| 4146 |
logging.warning(
|
| 4147 |
+
"Post-normalization Section lineage retry | unit=%s attempt=%s lineages=%s exhausted_siblings=%s count=%s types=%s",
|
| 4148 |
+
display_name, section_attempt, sorted(fresh_lineages), sorted(exhausted_lineages), len(retry_rows),
|
| 4149 |
+
sorted({str(row.get("type") or "") for row in retry_rows if isinstance(row, dict)}),
|
| 4150 |
)
|
| 4151 |
self._restore_section_citation_state(
|
| 4152 |
agent, citation_plan, display_name, frozen_citation_state
|
| 4153 |
)
|
| 4154 |
continue
|
| 4155 |
+
blocking_rows, advisory_rows = self._partition_section_violations_by_lineage(
|
| 4156 |
+
post_validation_rows, lineage_attempts, section_name=display_name, mode=mode,
|
| 4157 |
+
hard_cap_reached=hard_cap_reached
|
| 4158 |
+
)
|
| 4159 |
if advisory_rows:
|
| 4160 |
self._record_section_advisories(
|
| 4161 |
quality_notes, advisory_rows, section_name=display_name
|
core/pipeline_agent.py
CHANGED
|
@@ -8219,6 +8219,20 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
|
|
| 8219 |
'last_evidence_signature': str(meta.get('last_evidence_signature') or ''),
|
| 8220 |
'last_strategy': str(meta.get('last_strategy') or ''),
|
| 8221 |
'nonconvergent_state': str(meta.get('nonconvergent_state') or ''),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8222 |
}
|
| 8223 |
return states
|
| 8224 |
|
|
@@ -8736,7 +8750,8 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8736 |
current_target_claim = segments[claim_id]
|
| 8737 |
evidence_key = (block_id, claim_id)
|
| 8738 |
repair_evidence = dict(self._translation_claim_qa_evidence.get(evidence_key) or {})
|
| 8739 |
-
|
|
|
|
| 8740 |
meta = self._translation_claim_repair_meta.setdefault(evidence_key, {
|
| 8741 |
'repair_attempt_count': 0, 'last_issue_codes': [], 'last_evidence_signature': '',
|
| 8742 |
'last_strategy': '', 'nonconvergent_state': '',
|
|
@@ -8744,27 +8759,48 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8744 |
})
|
| 8745 |
prior_attempts = int(meta.get('repair_attempt_count') or 0)
|
| 8746 |
prior_codes = set(meta.get('last_issue_codes') or [])
|
| 8747 |
-
|
| 8748 |
family_counts = dict(meta.get('family_attempt_counts') or {})
|
| 8749 |
family_strategies = dict(meta.get('family_last_strategy') or {})
|
| 8750 |
exhausted_families = {
|
| 8751 |
-
family for family in
|
| 8752 |
if int(family_counts.get(family, 0)) >= self.MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS
|
| 8753 |
}
|
|
|
|
| 8754 |
claim_hard_cap = prior_attempts >= self.MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS
|
| 8755 |
-
if
|
| 8756 |
meta['nonconvergent_state'] = 'exhausted_family:' + ','.join(sorted(exhausted_families)) if exhausted_families else 'exhausted_after_claim_hard_cap'
|
| 8757 |
carried_exhausted.setdefault(block_id, {})[claim_id] = list(codes)
|
| 8758 |
logging.info(
|
| 8759 |
'Translation claim defect-family entitlement exhausted: section=%s id=SP-P%s-%s attempts=%s families=%s',
|
| 8760 |
-
section_name or 'section', block_id, claim_id, prior_attempts, sorted(exhausted_families or set(
|
| 8761 |
)
|
| 8762 |
self._record_translation_assurance(
|
| 8763 |
'CLAIM_REPAIR_EXHAUSTED', target_lang=target_lang,
|
| 8764 |
block_id=f'SP-P{block_id}-{claim_id}', severity='critical', decision='ADJUDICATE',
|
| 8765 |
-
detail={'attempts': prior_attempts, 'families': '|'.join(sorted(exhausted_families or set(
|
| 8766 |
)
|
| 8767 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8768 |
if prior_attempts == 0 and repair_cycle > 1:
|
| 8769 |
logging.info(
|
| 8770 |
'Translation late-detected claim receives own repair entitlement: section=%s id=SP-P%s-%s cycle=%s',
|
|
|
|
| 8219 |
'last_evidence_signature': str(meta.get('last_evidence_signature') or ''),
|
| 8220 |
'last_strategy': str(meta.get('last_strategy') or ''),
|
| 8221 |
'nonconvergent_state': str(meta.get('nonconvergent_state') or ''),
|
| 8222 |
+
# R6-R3: persist defect-family ownership across durable resume.
|
| 8223 |
+
# Without these fields a restarted RU/KK/ZH claim can collapse
|
| 8224 |
+
# back to legacy claim-level history and either revive an
|
| 8225 |
+
# exhausted family or deny a genuinely fresh family its turn.
|
| 8226 |
+
'family_attempt_counts': {
|
| 8227 |
+
str(family): int(count or 0)
|
| 8228 |
+
for family, count in dict(meta.get('family_attempt_counts') or {}).items()
|
| 8229 |
+
if str(family or '').strip()
|
| 8230 |
+
},
|
| 8231 |
+
'family_last_strategy': {
|
| 8232 |
+
str(family): str(strategy or '').strip().upper()
|
| 8233 |
+
for family, strategy in dict(meta.get('family_last_strategy') or {}).items()
|
| 8234 |
+
if str(family or '').strip()
|
| 8235 |
+
},
|
| 8236 |
}
|
| 8237 |
return states
|
| 8238 |
|
|
|
|
| 8750 |
current_target_claim = segments[claim_id]
|
| 8751 |
evidence_key = (block_id, claim_id)
|
| 8752 |
repair_evidence = dict(self._translation_claim_qa_evidence.get(evidence_key) or {})
|
| 8753 |
+
observed_code_set = {str(code or '').strip().upper() for code in (codes or [])}
|
| 8754 |
+
code_set = set(observed_code_set)
|
| 8755 |
meta = self._translation_claim_repair_meta.setdefault(evidence_key, {
|
| 8756 |
'repair_attempt_count': 0, 'last_issue_codes': [], 'last_evidence_signature': '',
|
| 8757 |
'last_strategy': '', 'nonconvergent_state': '',
|
|
|
|
| 8759 |
})
|
| 8760 |
prior_attempts = int(meta.get('repair_attempt_count') or 0)
|
| 8761 |
prior_codes = set(meta.get('last_issue_codes') or [])
|
| 8762 |
+
observed_families = self._translation_defect_family(code_set)
|
| 8763 |
family_counts = dict(meta.get('family_attempt_counts') or {})
|
| 8764 |
family_strategies = dict(meta.get('family_last_strategy') or {})
|
| 8765 |
exhausted_families = {
|
| 8766 |
+
family for family in observed_families
|
| 8767 |
if int(family_counts.get(family, 0)) >= self.MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS
|
| 8768 |
}
|
| 8769 |
+
eligible_families = set(observed_families) - exhausted_families
|
| 8770 |
claim_hard_cap = prior_attempts >= self.MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS
|
| 8771 |
+
if claim_hard_cap or not eligible_families:
|
| 8772 |
meta['nonconvergent_state'] = 'exhausted_family:' + ','.join(sorted(exhausted_families)) if exhausted_families else 'exhausted_after_claim_hard_cap'
|
| 8773 |
carried_exhausted.setdefault(block_id, {})[claim_id] = list(codes)
|
| 8774 |
logging.info(
|
| 8775 |
'Translation claim defect-family entitlement exhausted: section=%s id=SP-P%s-%s attempts=%s families=%s',
|
| 8776 |
+
section_name or 'section', block_id, claim_id, prior_attempts, sorted(exhausted_families or set(observed_families)),
|
| 8777 |
)
|
| 8778 |
self._record_translation_assurance(
|
| 8779 |
'CLAIM_REPAIR_EXHAUSTED', target_lang=target_lang,
|
| 8780 |
block_id=f'SP-P{block_id}-{claim_id}', severity='critical', decision='ADJUDICATE',
|
| 8781 |
+
detail={'attempts': prior_attempts, 'families': '|'.join(sorted(exhausted_families or set(observed_families)))},
|
| 8782 |
)
|
| 8783 |
continue
|
| 8784 |
+
if exhausted_families:
|
| 8785 |
+
# R6-R3 fairness closure: do not let an exhausted sibling
|
| 8786 |
+
# family consume or block the entitlement of a still-eligible
|
| 8787 |
+
# family. Repair only codes owned by eligible families; the
|
| 8788 |
+
# exhausted defect remains subject to later adjudication if it
|
| 8789 |
+
# survives the fresh-family repair.
|
| 8790 |
+
active_codes = [
|
| 8791 |
+
str(code or '').strip().upper() for code in (codes or [])
|
| 8792 |
+
if set(self._translation_defect_family([code])) & eligible_families
|
| 8793 |
+
]
|
| 8794 |
+
codes = list(dict.fromkeys(active_codes))
|
| 8795 |
+
code_set = set(codes)
|
| 8796 |
+
families = self._translation_defect_family(code_set)
|
| 8797 |
+
logging.info(
|
| 8798 |
+
'Translation mixed exhausted+eligible families: section=%s id=SP-P%s-%s exhausted=%s eligible=%s repair_codes=%s',
|
| 8799 |
+
section_name or 'section', block_id, claim_id, sorted(exhausted_families),
|
| 8800 |
+
sorted(eligible_families), sorted(code_set),
|
| 8801 |
+
)
|
| 8802 |
+
else:
|
| 8803 |
+
families = observed_families
|
| 8804 |
if prior_attempts == 0 and repair_cycle > 1:
|
| 8805 |
logging.info(
|
| 8806 |
'Translation late-detected claim receives own repair entitlement: section=%s id=SP-P%s-%s cycle=%s',
|