Spaces:
Running
Running
S0-R1-C5.5-R4 Cross-Mode Defect-Lineage Retry Ownership + Methodological Causal-Scope Closure
#87
by GuXiaopeng - opened
- core/generation_runner.py +75 -13
- core/pipeline_agent.py +71 -17
- core/quantitative_engine.py +48 -1
core/generation_runner.py
CHANGED
|
@@ -1035,6 +1035,48 @@ class GenerationRunner:
|
|
| 1035 |
# new to the severity registry.
|
| 1036 |
return "FATAL"
|
| 1037 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1038 |
@classmethod
|
| 1039 |
def _partition_section_violations(cls, rows, *, section_name: str = "", mode: str = "", exhausted: bool = False):
|
| 1040 |
blocking, advisory = [], []
|
|
@@ -3621,7 +3663,10 @@ Output only final academic prose. Do not report the word count."""
|
|
| 3621 |
raise RuntimeError(f"section_retry_exhausted_requires_new_lifecycle:{display_name}")
|
| 3622 |
|
| 3623 |
deterministic_appendix = mode == "asq" and section_id == "appendix"
|
| 3624 |
-
|
|
|
|
|
|
|
|
|
|
| 3625 |
frozen_citation_state = self._snapshot_section_citation_state(
|
| 3626 |
agent, citation_plan, display_name
|
| 3627 |
)
|
|
@@ -3752,19 +3797,27 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3752 |
)
|
| 3753 |
if phase1_validation_rows:
|
| 3754 |
retry_excluded_tags.update(self._citation_violation_tags(citation_semantic_rows))
|
| 3755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3756 |
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3757 |
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3758 |
)
|
| 3759 |
-
if blocking_rows and
|
|
|
|
|
|
|
| 3760 |
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 3761 |
logging.warning(
|
| 3762 |
-
"Unified Phase-1 Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3763 |
-
display_name, section_attempt, len(blocking_rows),
|
| 3764 |
-
sorted({
|
| 3765 |
-
str(row.get("type") or "")
|
| 3766 |
-
for row in blocking_rows if isinstance(row, dict)
|
| 3767 |
-
}),
|
| 3768 |
)
|
| 3769 |
self._restore_section_citation_state(
|
| 3770 |
agent, citation_plan, display_name, frozen_citation_state
|
|
@@ -3930,15 +3983,24 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3930 |
hypothesis_ledger=hypothesis_ledger,
|
| 3931 |
)
|
| 3932 |
if post_validation_rows:
|
| 3933 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3934 |
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3935 |
post_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3936 |
)
|
| 3937 |
-
if blocking_rows and
|
|
|
|
|
|
|
| 3938 |
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 3939 |
logging.warning(
|
| 3940 |
-
"Post-normalization Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3941 |
-
display_name, section_attempt, len(blocking_rows),
|
| 3942 |
sorted({str(row.get("type") or "") for row in blocking_rows if isinstance(row, dict)}),
|
| 3943 |
)
|
| 3944 |
self._restore_section_citation_state(
|
|
|
|
| 1035 |
# new to the severity registry.
|
| 1036 |
return "FATAL"
|
| 1037 |
|
| 1038 |
+
@staticmethod
|
| 1039 |
+
def _section_violation_lineage(row) -> str:
|
| 1040 |
+
"""Return a stable repair lineage for one section validation defect.
|
| 1041 |
+
|
| 1042 |
+
R4 assigns the bounded rewrite entitlement to the defect lineage rather
|
| 1043 |
+
than to the whole Section. Closely related validators share a lineage
|
| 1044 |
+
so spelling variants of the same integrity problem cannot manufacture
|
| 1045 |
+
fresh retries.
|
| 1046 |
+
"""
|
| 1047 |
+
item = dict(row or {}) if isinstance(row, dict) else {"type": "unknown_validation_issue"}
|
| 1048 |
+
kind = str(item.get("type") or "unknown_validation_issue")
|
| 1049 |
+
groups = {
|
| 1050 |
+
"citation": {"citation_claim_split_required", "citation_unsupported_claim", "citation_support_unverified",
|
| 1051 |
+
"citation_outside_frozen_section_whitelist"},
|
| 1052 |
+
"causal_scope": {"causal_language_violation"},
|
| 1053 |
+
"quant_method": {"unsupported_quantitative_method", "quantitative_metric_mismatch", "unfrozen_p_value"},
|
| 1054 |
+
"study_fact": {"sample_size_mismatch", "sample_count_mismatch", "scale_points_mismatch",
|
| 1055 |
+
"construct_role_mismatch", "actor_data_source_relation_mismatch", "unsupported_study_procedure"},
|
| 1056 |
+
"enumeration": {"enumeration_contract_invalid", "enumeration_contract_mismatch"},
|
| 1057 |
+
"hypothesis": {"hypothesis_contract_missing_id", "hypothesis_contract_ordinal_alias",
|
| 1058 |
+
"hypothesis_contract_pairing_mismatch", "hypothesis_contract_unknown_id"},
|
| 1059 |
+
"workflow_trace": {"authorial_ai_meta_trace", "author_facing_internal_workflow_language"},
|
| 1060 |
+
}
|
| 1061 |
+
for lineage, kinds in groups.items():
|
| 1062 |
+
if kind in kinds:
|
| 1063 |
+
return lineage
|
| 1064 |
+
return kind
|
| 1065 |
+
|
| 1066 |
+
@classmethod
|
| 1067 |
+
def _section_retry_decision(cls, rows, lineage_attempts, *, hard_cap_reached: bool = False):
|
| 1068 |
+
"""Return (may_retry, exhausted_lineages, new_lineages).
|
| 1069 |
+
|
| 1070 |
+
Each lineage receives one rewrite entitlement. A lineage that survives
|
| 1071 |
+
its own rewrite remains fail-closed. A genuinely new lineage discovered
|
| 1072 |
+
only after a previous repair receives one bounded chance, subject to the
|
| 1073 |
+
Section hard cap.
|
| 1074 |
+
"""
|
| 1075 |
+
lineages = {cls._section_violation_lineage(row) for row in (rows or [])}
|
| 1076 |
+
exhausted = {name for name in lineages if int(lineage_attempts.get(name, 0)) >= 1}
|
| 1077 |
+
fresh = lineages - exhausted
|
| 1078 |
+
return bool(fresh) and not exhausted and not hard_cap_reached, exhausted, fresh
|
| 1079 |
+
|
| 1080 |
@classmethod
|
| 1081 |
def _partition_section_violations(cls, rows, *, section_name: str = "", mode: str = "", exhausted: bool = False):
|
| 1082 |
blocking, advisory = [], []
|
|
|
|
| 3663 |
raise RuntimeError(f"section_retry_exhausted_requires_new_lifecycle:{display_name}")
|
| 3664 |
|
| 3665 |
deterministic_appendix = mode == "asq" and section_id == "appendix"
|
| 3666 |
+
# R4: one initial attempt plus bounded lineage-owned repair turns.
|
| 3667 |
+
# Four is a hard safety cap, not a blanket four-attempt entitlement.
|
| 3668 |
+
max_section_attempts = 1 if deterministic_appendix else 4
|
| 3669 |
+
lineage_attempts = {}
|
| 3670 |
frozen_citation_state = self._snapshot_section_citation_state(
|
| 3671 |
agent, citation_plan, display_name
|
| 3672 |
)
|
|
|
|
| 3797 |
)
|
| 3798 |
if phase1_validation_rows:
|
| 3799 |
retry_excluded_tags.update(self._citation_violation_tags(citation_semantic_rows))
|
| 3800 |
+
hard_cap_reached = section_attempt >= max_section_attempts
|
| 3801 |
+
# Severity is evaluated as exhausted only when this defect
|
| 3802 |
+
# lineage has already consumed its own repair entitlement.
|
| 3803 |
+
preview_blocking, preview_advisory = self._partition_section_violations(
|
| 3804 |
+
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=False
|
| 3805 |
+
)
|
| 3806 |
+
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 3807 |
+
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 3808 |
+
)
|
| 3809 |
+
exhausted = hard_cap_reached or bool(exhausted_lineages)
|
| 3810 |
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3811 |
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3812 |
)
|
| 3813 |
+
if blocking_rows and may_retry:
|
| 3814 |
+
for lineage in fresh_lineages:
|
| 3815 |
+
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 3816 |
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 3817 |
logging.warning(
|
| 3818 |
+
"Unified Phase-1 Section lineage retry | unit=%s attempt=%s lineages=%s count=%s types=%s",
|
| 3819 |
+
display_name, section_attempt, sorted(fresh_lineages), len(blocking_rows),
|
| 3820 |
+
sorted({str(row.get("type") or "") for row in blocking_rows if isinstance(row, dict)}),
|
|
|
|
|
|
|
|
|
|
| 3821 |
)
|
| 3822 |
self._restore_section_citation_state(
|
| 3823 |
agent, citation_plan, display_name, frozen_citation_state
|
|
|
|
| 3983 |
hypothesis_ledger=hypothesis_ledger,
|
| 3984 |
)
|
| 3985 |
if post_validation_rows:
|
| 3986 |
+
hard_cap_reached = section_attempt >= max_section_attempts
|
| 3987 |
+
preview_blocking, preview_advisory = self._partition_section_violations(
|
| 3988 |
+
post_validation_rows, section_name=display_name, mode=mode, exhausted=False
|
| 3989 |
+
)
|
| 3990 |
+
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 3991 |
+
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 3992 |
+
)
|
| 3993 |
+
exhausted = hard_cap_reached or bool(exhausted_lineages)
|
| 3994 |
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3995 |
post_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3996 |
)
|
| 3997 |
+
if blocking_rows and may_retry:
|
| 3998 |
+
for lineage in fresh_lineages:
|
| 3999 |
+
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 4000 |
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 4001 |
logging.warning(
|
| 4002 |
+
"Post-normalization Section lineage retry | unit=%s attempt=%s lineages=%s count=%s types=%s",
|
| 4003 |
+
display_name, section_attempt, sorted(fresh_lineages), len(blocking_rows),
|
| 4004 |
sorted({str(row.get("type") or "") for row in blocking_rows if isinstance(row, dict)}),
|
| 4005 |
)
|
| 4006 |
self._restore_section_citation_state(
|
core/pipeline_agent.py
CHANGED
|
@@ -72,9 +72,33 @@ class CitationAuditError(DeepSeekRequestError):
|
|
| 72 |
class SSciPipelineAgent:
|
| 73 |
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
|
| 74 |
MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
|
|
|
|
| 75 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 76 |
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v1'
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
def __init__(self, ds_key, s2_key=""):
|
| 79 |
self.ds_key = str(ds_key or "").strip()
|
| 80 |
if not self.ds_key:
|
|
@@ -8268,12 +8292,26 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8268 |
claim_key = str(row.get('claim_id') or '')
|
| 8269 |
if not block_key or not claim_key:
|
| 8270 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8271 |
self._translation_claim_repair_meta[(block_key, claim_key)] = {
|
| 8272 |
-
'repair_attempt_count':
|
| 8273 |
-
'last_issue_codes':
|
| 8274 |
'last_evidence_signature': str(row.get('last_evidence_signature') or ''),
|
| 8275 |
'last_strategy': str(row.get('last_strategy') or '').strip().upper(),
|
| 8276 |
-
'nonconvergent_state':
|
|
|
|
|
|
|
| 8277 |
}
|
| 8278 |
|
| 8279 |
def emit_stage(stage):
|
|
@@ -8330,7 +8368,7 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8330 |
)
|
| 8331 |
repair_cycle = 0
|
| 8332 |
total_claims = sum(len(self._translation_claim_units(source)) for source in source_blocks.values())
|
| 8333 |
-
max_repair_cycles = max(1, total_claims * self.
|
| 8334 |
while quality_issues and repair_cycle < max_repair_cycles:
|
| 8335 |
repair_cycle += 1
|
| 8336 |
failed_pairs = [
|
|
@@ -8366,26 +8404,29 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8366 |
meta = self._translation_claim_repair_meta.setdefault(evidence_key, {
|
| 8367 |
'repair_attempt_count': 0, 'last_issue_codes': [], 'last_evidence_signature': '',
|
| 8368 |
'last_strategy': '', 'nonconvergent_state': '',
|
|
|
|
| 8369 |
})
|
| 8370 |
prior_attempts = int(meta.get('repair_attempt_count') or 0)
|
| 8371 |
prior_codes = set(meta.get('last_issue_codes') or [])
|
| 8372 |
-
|
| 8373 |
-
|
| 8374 |
-
|
| 8375 |
-
|
| 8376 |
-
|
| 8377 |
-
|
| 8378 |
-
|
| 8379 |
-
|
|
|
|
|
|
|
| 8380 |
carried_exhausted.setdefault(block_id, {})[claim_id] = list(codes)
|
| 8381 |
logging.info(
|
| 8382 |
-
'Translation claim
|
| 8383 |
-
section_name or 'section', block_id, claim_id, prior_attempts,
|
| 8384 |
)
|
| 8385 |
self._record_translation_assurance(
|
| 8386 |
'CLAIM_REPAIR_EXHAUSTED', target_lang=target_lang,
|
| 8387 |
block_id=f'SP-P{block_id}-{claim_id}', severity='critical', decision='ADJUDICATE',
|
| 8388 |
-
detail={'attempts': prior_attempts, '
|
| 8389 |
)
|
| 8390 |
continue
|
| 8391 |
if prior_attempts == 0 and repair_cycle > 1:
|
|
@@ -8485,9 +8526,15 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8485 |
'UNRESOLVED_SEMANTIC_DELTA' in set(repair_evidence.get('semantic_delta_codes') or [])
|
| 8486 |
or 'GRAMMAR_UNSPECIFIED' in set(repair_evidence.get('grammar_evidence_codes') or [])
|
| 8487 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8488 |
strategy = 'SOURCE_REGENERATE' if (
|
| 8489 |
force_source_regenerate
|
| 8490 |
-
or
|
|
|
|
| 8491 |
or (same_defect and str(meta.get('last_strategy') or '').upper() == 'LOCAL_EDIT')
|
| 8492 |
) else 'LOCAL_EDIT'
|
| 8493 |
if strategy == 'SOURCE_REGENERATE':
|
|
@@ -8517,10 +8564,17 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8517 |
**repair_kwargs,
|
| 8518 |
)
|
| 8519 |
meta['repair_attempt_count'] = int(meta.get('repair_attempt_count') or 0) + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8520 |
meta['last_issue_codes'] = sorted(code_set)[:5]
|
| 8521 |
meta['last_evidence_signature'] = signature
|
| 8522 |
meta['last_strategy'] = strategy
|
| 8523 |
-
meta['nonconvergent_state'] = '
|
| 8524 |
if 'LANGUAGE_CONTAMINATION' in code_set:
|
| 8525 |
post_repair_lexical = self._translation_lexical_purity_evidence(
|
| 8526 |
source_claims[claim_id], repaired_claim, target_lang, frozen_glossary
|
|
|
|
| 72 |
class SSciPipelineAgent:
|
| 73 |
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
|
| 74 |
MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
|
| 75 |
+
MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
|
| 76 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 77 |
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v1'
|
| 78 |
|
| 79 |
+
@staticmethod
|
| 80 |
+
def _translation_defect_family(codes):
|
| 81 |
+
"""Collapse translation QA codes into stable repair lineages.
|
| 82 |
+
|
| 83 |
+
R4 gives each claim+defect family its own bounded entitlement while
|
| 84 |
+
retaining a claim-level hard cap. Closely related semantic codes share
|
| 85 |
+
one family so re-labeling the same defect cannot reset the budget.
|
| 86 |
+
"""
|
| 87 |
+
normalized = {str(code or '').strip().upper() for code in (codes or []) if str(code or '').strip()}
|
| 88 |
+
families = []
|
| 89 |
+
if normalized & {'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT','UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM'}:
|
| 90 |
+
families.append('SEMANTIC')
|
| 91 |
+
if 'GRAMMAR' in normalized:
|
| 92 |
+
families.append('GRAMMAR')
|
| 93 |
+
if 'TERMINOLOGY' in normalized:
|
| 94 |
+
families.append('TERMINOLOGY')
|
| 95 |
+
if 'LANGUAGE_CONTAMINATION' in normalized:
|
| 96 |
+
families.append('LANGUAGE_CONTAMINATION')
|
| 97 |
+
for code in sorted(normalized):
|
| 98 |
+
if code not in {'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT','UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM','GRAMMAR','TERMINOLOGY','LANGUAGE_CONTAMINATION'}:
|
| 99 |
+
families.append(code)
|
| 100 |
+
return tuple(dict.fromkeys(families)) or ('UNKNOWN',)
|
| 101 |
+
|
| 102 |
def __init__(self, ds_key, s2_key=""):
|
| 103 |
self.ds_key = str(ds_key or "").strip()
|
| 104 |
if not self.ds_key:
|
|
|
|
| 8292 |
claim_key = str(row.get('claim_id') or '')
|
| 8293 |
if not block_key or not claim_key:
|
| 8294 |
continue
|
| 8295 |
+
prior_codes = [str(code or '').strip().upper() for code in (row.get('last_issue_codes') or []) if str(code or '').strip()][:5]
|
| 8296 |
+
family_attempt_counts = dict(row.get('family_attempt_counts') or {})
|
| 8297 |
+
family_last_strategy = dict(row.get('family_last_strategy') or {})
|
| 8298 |
+
legacy_state = str(row.get('nonconvergent_state') or '')
|
| 8299 |
+
legacy_attempts = int(row.get('repair_attempt_count') or 0)
|
| 8300 |
+
# R4 migration rule: an R3 checkpoint that had already exhausted a
|
| 8301 |
+
# claim/family remains fail-closed. New lineage ownership must never
|
| 8302 |
+
# resurrect a previously terminal defect.
|
| 8303 |
+
if not family_attempt_counts and (legacy_state.startswith('exhausted') or legacy_attempts >= self.MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS):
|
| 8304 |
+
for family in self._translation_defect_family(prior_codes):
|
| 8305 |
+
family_attempt_counts[family] = self.MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS
|
| 8306 |
+
family_last_strategy[family] = str(row.get('last_strategy') or '').strip().upper()
|
| 8307 |
self._translation_claim_repair_meta[(block_key, claim_key)] = {
|
| 8308 |
+
'repair_attempt_count': legacy_attempts,
|
| 8309 |
+
'last_issue_codes': prior_codes,
|
| 8310 |
'last_evidence_signature': str(row.get('last_evidence_signature') or ''),
|
| 8311 |
'last_strategy': str(row.get('last_strategy') or '').strip().upper(),
|
| 8312 |
+
'nonconvergent_state': legacy_state,
|
| 8313 |
+
'family_attempt_counts': family_attempt_counts,
|
| 8314 |
+
'family_last_strategy': family_last_strategy,
|
| 8315 |
}
|
| 8316 |
|
| 8317 |
def emit_stage(stage):
|
|
|
|
| 8368 |
)
|
| 8369 |
repair_cycle = 0
|
| 8370 |
total_claims = sum(len(self._translation_claim_units(source)) for source in source_blocks.values())
|
| 8371 |
+
max_repair_cycles = max(1, total_claims * self.MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS)
|
| 8372 |
while quality_issues and repair_cycle < max_repair_cycles:
|
| 8373 |
repair_cycle += 1
|
| 8374 |
failed_pairs = [
|
|
|
|
| 8404 |
meta = self._translation_claim_repair_meta.setdefault(evidence_key, {
|
| 8405 |
'repair_attempt_count': 0, 'last_issue_codes': [], 'last_evidence_signature': '',
|
| 8406 |
'last_strategy': '', 'nonconvergent_state': '',
|
| 8407 |
+
'family_attempt_counts': {}, 'family_last_strategy': {},
|
| 8408 |
})
|
| 8409 |
prior_attempts = int(meta.get('repair_attempt_count') or 0)
|
| 8410 |
prior_codes = set(meta.get('last_issue_codes') or [])
|
| 8411 |
+
families = self._translation_defect_family(code_set)
|
| 8412 |
+
family_counts = dict(meta.get('family_attempt_counts') or {})
|
| 8413 |
+
family_strategies = dict(meta.get('family_last_strategy') or {})
|
| 8414 |
+
exhausted_families = {
|
| 8415 |
+
family for family in families
|
| 8416 |
+
if int(family_counts.get(family, 0)) >= self.MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS
|
| 8417 |
+
}
|
| 8418 |
+
claim_hard_cap = prior_attempts >= self.MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS
|
| 8419 |
+
if exhausted_families or claim_hard_cap:
|
| 8420 |
+
meta['nonconvergent_state'] = 'exhausted_family:' + ','.join(sorted(exhausted_families)) if exhausted_families else 'exhausted_after_claim_hard_cap'
|
| 8421 |
carried_exhausted.setdefault(block_id, {})[claim_id] = list(codes)
|
| 8422 |
logging.info(
|
| 8423 |
+
'Translation claim defect-family entitlement exhausted: section=%s id=SP-P%s-%s attempts=%s families=%s',
|
| 8424 |
+
section_name or 'section', block_id, claim_id, prior_attempts, sorted(exhausted_families or set(families)),
|
| 8425 |
)
|
| 8426 |
self._record_translation_assurance(
|
| 8427 |
'CLAIM_REPAIR_EXHAUSTED', target_lang=target_lang,
|
| 8428 |
block_id=f'SP-P{block_id}-{claim_id}', severity='critical', decision='ADJUDICATE',
|
| 8429 |
+
detail={'attempts': prior_attempts, 'families': '|'.join(sorted(exhausted_families or set(families)))},
|
| 8430 |
)
|
| 8431 |
continue
|
| 8432 |
if prior_attempts == 0 and repair_cycle > 1:
|
|
|
|
| 8526 |
'UNRESOLVED_SEMANTIC_DELTA' in set(repair_evidence.get('semantic_delta_codes') or [])
|
| 8527 |
or 'GRAMMAR_UNSPECIFIED' in set(repair_evidence.get('grammar_evidence_codes') or [])
|
| 8528 |
)
|
| 8529 |
+
family_prior_attempts = max(int(family_counts.get(family, 0)) for family in families)
|
| 8530 |
+
family_prior_source_regenerated = any(
|
| 8531 |
+
str(family_strategies.get(family) or '').upper() == 'SOURCE_REGENERATE'
|
| 8532 |
+
for family in families
|
| 8533 |
+
)
|
| 8534 |
strategy = 'SOURCE_REGENERATE' if (
|
| 8535 |
force_source_regenerate
|
| 8536 |
+
or family_prior_attempts >= 1
|
| 8537 |
+
or family_prior_source_regenerated
|
| 8538 |
or (same_defect and str(meta.get('last_strategy') or '').upper() == 'LOCAL_EDIT')
|
| 8539 |
) else 'LOCAL_EDIT'
|
| 8540 |
if strategy == 'SOURCE_REGENERATE':
|
|
|
|
| 8564 |
**repair_kwargs,
|
| 8565 |
)
|
| 8566 |
meta['repair_attempt_count'] = int(meta.get('repair_attempt_count') or 0) + 1
|
| 8567 |
+
family_counts = dict(meta.get('family_attempt_counts') or family_counts)
|
| 8568 |
+
family_strategies = dict(meta.get('family_last_strategy') or family_strategies)
|
| 8569 |
+
for family in families:
|
| 8570 |
+
family_counts[family] = int(family_counts.get(family, 0)) + 1
|
| 8571 |
+
family_strategies[family] = strategy
|
| 8572 |
+
meta['family_attempt_counts'] = family_counts
|
| 8573 |
+
meta['family_last_strategy'] = family_strategies
|
| 8574 |
meta['last_issue_codes'] = sorted(code_set)[:5]
|
| 8575 |
meta['last_evidence_signature'] = signature
|
| 8576 |
meta['last_strategy'] = strategy
|
| 8577 |
+
meta['nonconvergent_state'] = ''
|
| 8578 |
if 'LANGUAGE_CONTAMINATION' in code_set:
|
| 8579 |
post_repair_lexical = self._translation_lexical_purity_evidence(
|
| 8580 |
source_claims[claim_id], repaired_claim, target_lang, frozen_glossary
|
core/quantitative_engine.py
CHANGED
|
@@ -1091,6 +1091,47 @@ Keywords: {keywords}"""
|
|
| 1091 |
"""Compatibility wrapper for older call sites; uses the shared tri-state contract."""
|
| 1092 |
return cls.classify_quantitative_causal_sentence(sentence) == "EXPLICIT_CAUSAL_LIMITATION"
|
| 1093 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
@classmethod
|
| 1095 |
def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
|
| 1096 |
"""Pure AMJ quantitative Evidence Ledger inspection; never rewrites prose."""
|
|
@@ -1162,7 +1203,13 @@ Keywords: {keywords}"""
|
|
| 1162 |
if pattern.search(sentence):
|
| 1163 |
violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
|
| 1164 |
if strict:
|
| 1165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1166 |
if causal_state == "AFFIRMATIVE_CAUSAL":
|
| 1167 |
violations.append({
|
| 1168 |
"type":"causal_language_violation",
|
|
|
|
| 1091 |
"""Compatibility wrapper for older call sites; uses the shared tri-state contract."""
|
| 1092 |
return cls.classify_quantitative_causal_sentence(sentence) == "EXPLICIT_CAUSAL_LIMITATION"
|
| 1093 |
|
| 1094 |
+
@classmethod
|
| 1095 |
+
def _is_methodological_bias_mechanism_sentence(cls, sentence, known_constructs=None):
|
| 1096 |
+
"""Recognize measurement/method-bias mechanisms without weakening construct causality.
|
| 1097 |
+
|
| 1098 |
+
R4 separates methodological statements about how measurement conditions
|
| 1099 |
+
may bias statistical quantities from substantive claims that one study
|
| 1100 |
+
construct causes another. The exemption is intentionally narrow: a
|
| 1101 |
+
measurement/source-bias actor and a statistical/measurement target must
|
| 1102 |
+
both be present. Explicit construct-level causal language still wins.
|
| 1103 |
+
"""
|
| 1104 |
+
value = re.sub(r"\s+", " ", str(sentence or "")).strip().casefold()
|
| 1105 |
+
if not value:
|
| 1106 |
+
return False
|
| 1107 |
+
methodological_actor = re.search(
|
| 1108 |
+
r"\b(?:self[- ]reports?|single\s+questionnaire|same[- ]source|common\s+method(?:\s+bias)?|"
|
| 1109 |
+
r"response\s+styles?|social\s+desirability|measurement\s+error|item\s+wording|"
|
| 1110 |
+
r"common\s+rater|method\s+variance|single[- ]source\s+measurement)\b", value, re.I
|
| 1111 |
+
)
|
| 1112 |
+
statistical_target = re.search(
|
| 1113 |
+
r"\b(?:correlations?|covariances?|associations?|estimates?|coefficients?|variance|"
|
| 1114 |
+
r"standard\s+errors?|reliability|validity|measurement\s+error|method\s+variance)\b", value, re.I
|
| 1115 |
+
)
|
| 1116 |
+
mechanism = re.search(
|
| 1117 |
+
r"\b(?:inflate|inflates|inflated|attenuate|attenuates|attenuated|bias|biases|biased|"
|
| 1118 |
+
r"distort|distorts|distorted|increase|increases|increased|decrease|decreases|decreased|"
|
| 1119 |
+
r"contribute|contributes|contributed|induce|induces|induced)\b", value, re.I
|
| 1120 |
+
)
|
| 1121 |
+
if not (methodological_actor and statistical_target and mechanism):
|
| 1122 |
+
return False
|
| 1123 |
+
|
| 1124 |
+
# Never let a methodological phrase shield an explicit relation between
|
| 1125 |
+
# named study constructs.
|
| 1126 |
+
constructs = [str(x or '').strip().casefold() for x in (known_constructs or []) if str(x or '').strip()]
|
| 1127 |
+
named_hits = [c for c in constructs if c and c in value]
|
| 1128 |
+
if len(named_hits) >= 2 and re.search(
|
| 1129 |
+
r"\b(?:causes?|leads?\s+to|drives?|determines?|produces?|results?\s+in|yields?|generates?)\b",
|
| 1130 |
+
value, re.I
|
| 1131 |
+
):
|
| 1132 |
+
return False
|
| 1133 |
+
return True
|
| 1134 |
+
|
| 1135 |
@classmethod
|
| 1136 |
def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
|
| 1137 |
"""Pure AMJ quantitative Evidence Ledger inspection; never rewrites prose."""
|
|
|
|
| 1203 |
if pattern.search(sentence):
|
| 1204 |
violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
|
| 1205 |
if strict:
|
| 1206 |
+
methodological_bias_scope = cls._is_methodological_bias_mechanism_sentence(
|
| 1207 |
+
sentence, known_constructs
|
| 1208 |
+
)
|
| 1209 |
+
causal_state = (
|
| 1210 |
+
"NON_CAUSAL" if methodological_bias_scope
|
| 1211 |
+
else cls.classify_quantitative_causal_sentence(sentence)
|
| 1212 |
+
)
|
| 1213 |
if causal_state == "AFFIRMATIVE_CAUSAL":
|
| 1214 |
violations.append({
|
| 1215 |
"type":"causal_language_violation",
|