S1-R3A Final Abstract Resilience + RU-KK Title Fallback

#124
core/generation_runner.py CHANGED
@@ -1053,7 +1053,8 @@ class GenerationRunner:
1053
  r"\bfindings?\s+(?:show|shows|showed|indicate|indicates|indicated|reveal|reveals|revealed|identify|identifies|identified|suggest|suggests|suggested)\b|"
1054
  r"\b(?:was|were|is|are)\s+(?:positively|negatively|significantly|non[- ]?significantly|not\s+significantly)\s+(?:associated|related)\b|"
1055
  r"\b(?:association|relationship|interaction|indirect\s+(?:association|effect)|theme|themes|pattern|patterns|mechanism|mechanisms)\b[^.!?]{0,140}\b(?:was|were|is|are|emerged|identified|supported|unsupported|significant|non[- ]?significant|nonzero)\b|"
1056
- r"\bH\d+\s+(?:was|were|is|are)\s+(?:supported|unsupported|not\s+supported|rejected)\b"
 
1057
  r")"
1058
  )
1059
  result_sentences = []
@@ -1065,7 +1066,8 @@ class GenerationRunner:
1065
  retrospective = re.compile(
1066
  r"(?i)\b(?:results?|findings?\s+(?:show|indicate|reveal|identify|suggest)|"
1067
  r"analysis\s+(?:show|indicate|reveal|identify)|found|revealed|indicated|"
1068
- r"supported|unsupported|non[- ]?significant|not\s+significant|nonzero|received\s+no\s+support)\b"
 
1069
  )
1070
  for sentence in sentences:
1071
  if not finding_pattern.search(sentence):
@@ -1100,8 +1102,10 @@ class GenerationRunner:
1100
  if status == "unsupported":
1101
  return bool(re.search(
1102
  r"(?i)\b(?:not\s+supported|unsupported|no\s+support|did\s+not\s+support|"
1103
- r"non[- ]?significant|not\s+significant|no\s+(?:meaningful\s+)?(?:association|relationship|interaction|moderation|effect|evidence)|"
1104
- r"did\s+not\s+(?:moderate|differ|vary)|confidence\s+interval[^.!?]{0,80}(?:included|crossed)\s+zero)\b",
 
 
1105
  value,
1106
  ))
1107
  if status == "supported":
@@ -1113,6 +1117,232 @@ class GenerationRunner:
1113
  ))
1114
  return True
1115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1116
  @classmethod
1117
  def _final_abstract_finding_violations(
1118
  cls, mode: str, text: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None
@@ -1679,6 +1909,27 @@ class GenerationRunner:
1679
  rows.extend(self._final_abstract_finding_violations(
1680
  mode, repaired, data_metadata, hypothesis_ledger=hypothesis_ledger
1681
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682
  current, remaining = repaired, rows
1683
  if not remaining:
1684
  return current, []
@@ -2249,25 +2500,27 @@ class GenerationRunner:
2249
  if 'moderation' in missing:
2250
  label = role_label.get('moderator') or ''
2251
  if code.startswith('zh'):
2252
- suffixes.append(f'调节变量:{label}' if label else '调节作用')
2253
  elif code.startswith('ru'):
2254
- suffixes.append(f'модератор: {label}' if label else 'модерация')
2255
  elif code.startswith('kk') or code.startswith('kz'):
2256
- suffixes.append(f'модератор: {label}' if label else 'модераторлық рөл')
2257
  else:
2258
- suffixes.append(f'moderator: {label}' if label else 'moderation')
2259
  if missing.intersection({'mediation', 'serial_mediation', 'parallel_mediation'}):
2260
  label = role_label.get('mediator') or ''
2261
  if code.startswith('zh'):
2262
- suffixes.append(f'中介变量:{label}' if label else '中介作用')
2263
  elif code.startswith('ru'):
2264
- suffixes.append(f'медиатор: {label}' if label else 'медиация')
2265
  elif code.startswith('kk') or code.startswith('kz'):
2266
- suffixes.append(f'медиатор: {label}' if label else 'медиаторлық рөл')
2267
  else:
2268
- suffixes.append(f'mediator: {label}' if label else 'mediation')
2269
  if suffixes:
2270
- repaired_title = title.rstrip(' .;:') + ' (' + '; '.join(suffixes) + ')'
 
 
2271
  value = value[:title_match.start(1)] + repaired_title + value[title_match.end(1):]
2272
  diagnostics.append({'type':'target_user_relation_title_rebound','missing':sorted(missing)})
2273
  return value, diagnostics
@@ -5663,6 +5916,29 @@ The previous attempt for this same manuscript unit was rejected by validation. R
5663
  remaining_final_fact_rows.extend(self._final_abstract_finding_violations(
5664
  mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger
5665
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5666
  if remaining_final_fact_rows:
5667
  final_abstract, residual_final_rows = self._adaptive_repair_front_matter(
5668
  agent=agent, quant_engine=quant_engine, abstract_text=final_abstract,
 
1053
  r"\bfindings?\s+(?:show|shows|showed|indicate|indicates|indicated|reveal|reveals|revealed|identify|identifies|identified|suggest|suggests|suggested)\b|"
1054
  r"\b(?:was|were|is|are)\s+(?:positively|negatively|significantly|non[- ]?significantly|not\s+significantly)\s+(?:associated|related)\b|"
1055
  r"\b(?:association|relationship|interaction|indirect\s+(?:association|effect)|theme|themes|pattern|patterns|mechanism|mechanisms)\b[^.!?]{0,140}\b(?:was|were|is|are|emerged|identified|supported|unsupported|significant|non[- ]?significant|nonzero)\b|"
1056
+ r"\bH\d+\s+(?:was|were|is|are)\s+(?:supported|unsupported|not\s+supported|rejected)\b|"
1057
+ r"\bdid\s+not\s+(?:significantly\s+)?(?:moderate|mediate|change|alter)\b"
1058
  r")"
1059
  )
1060
  result_sentences = []
 
1066
  retrospective = re.compile(
1067
  r"(?i)\b(?:results?|findings?\s+(?:show|indicate|reveal|identify|suggest)|"
1068
  r"analysis\s+(?:show|indicate|reveal|identify)|found|revealed|indicated|"
1069
+ r"supported|unsupported|non[- ]?significant|not\s+significant|nonzero|received\s+no\s+support|"
1070
+ r"did\s+not\s+(?:significantly\s+)?(?:moderate|mediate|change|alter))\b"
1071
  )
1072
  for sentence in sentences:
1073
  if not finding_pattern.search(sentence):
 
1102
  if status == "unsupported":
1103
  return bool(re.search(
1104
  r"(?i)\b(?:not\s+supported|unsupported|no\s+support|did\s+not\s+support|"
1105
+ r"non[- ]?significant|not\s+significant|not\s+significantly\s+(?:associated|related)|"
1106
+ r"no\s+(?:meaningful\s+)?(?:association|relationship|interaction|moderation|effect|evidence)|"
1107
+ r"did\s+not\s+(?:significantly\s+)?(?:moderate|mediate|differ|vary|change|alter)|"
1108
+ r"confidence\s+interval[^.!?]{0,80}(?:included|crossed)\s+zero)\b",
1109
  value,
1110
  ))
1111
  if status == "supported":
 
1117
  ))
1118
  return True
1119
 
1120
+ @staticmethod
1121
+ def _final_abstract_metric_light(text: str) -> tuple[str, list[dict]]:
1122
+ """Remove nonessential inferential statistics from a Final Abstract.
1123
+
1124
+ This is a completion-resilience transform, not a correctness waiver.
1125
+ It is used only after a frozen Final Abstract has already been generated
1126
+ and a strict study-fact validator reports a metric-specific defect. The
1127
+ substantive finding sentence is preserved while beta/CI/p-value detail
1128
+ that is optional in an abstract is removed. Sample-size/design facts are
1129
+ intentionally untouched.
1130
+ """
1131
+ value = str(text or "")
1132
+ original = value
1133
+
1134
+ # Parenthetical inferential bundles are the common case:
1135
+ # (β = .21, 95% CI [...], p = .03)
1136
+ metric_bundle = re.compile(
1137
+ r"\s*\((?=[^()]{0,220}(?:β|standardized\s+estimate|(?:\d{2}%\s*)?CI\b|confidence\s+interval|\bp\s*[=<>]))[^()]*\)",
1138
+ flags=re.I,
1139
+ )
1140
+ value = metric_bundle.sub("", value)
1141
+
1142
+ # Bare inline statistics are also optional in the abstract. Keep the
1143
+ # surrounding finding language and remove only the metric surface.
1144
+ value = re.sub(
1145
+ r"(?i)(?:,\s*)?(?:β|standardized\s+estimate)\s*(?:=|:)?\s*[-+]?\d+(?:\.\d+)?",
1146
+ "",
1147
+ value,
1148
+ )
1149
+ value = re.sub(
1150
+ r"(?i)(?:,\s*)?(?:\d{2}%\s*)?(?:CI|confidence\s+interval)\s*[:=]?\s*[\[(][^\])]{1,80}[\])]?",
1151
+ "",
1152
+ value,
1153
+ )
1154
+ value = re.sub(r"(?i)(?:,\s*)?\bp\s*(?:=|<|>)\s*\.?\d+(?:\.\d+)?", "", value)
1155
+
1156
+ # Typography cleanup only; never alter construct identities or finding
1157
+ # polarity/support language.
1158
+ value = re.sub(r"\s+,", ",", value)
1159
+ value = re.sub(r",\s*,+", ",", value)
1160
+ value = re.sub(r"\(\s*\)", "", value)
1161
+ value = re.sub(r"\s+([.;:])", r"\1", value)
1162
+ value = re.sub(r" {2,}", " ", value)
1163
+ value = re.sub(r"\n{3,}", "\n\n", value).strip()
1164
+
1165
+ diagnostics = []
1166
+ if value != original.strip():
1167
+ diagnostics.append({
1168
+ "type": "final_abstract_metric_light_recovery",
1169
+ "reason": (
1170
+ "Optional inferential statistics were removed from the Final Abstract after strict metric validation could not safely bind them."
1171
+ ),
1172
+ })
1173
+ return value, diagnostics
1174
+
1175
+ @classmethod
1176
+ def _canonical_final_abstract_findings(
1177
+ cls, mode: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None,
1178
+ violation_types: set[str] | None = None,
1179
+ ) -> list[str]:
1180
+ """Return fact-safe, metric-light finding sentences from frozen ledgers.
1181
+
1182
+ These sentences are a last-resort completion fallback after DeepSeek has
1183
+ already received bounded local-repair opportunities. They use only
1184
+ frozen relation/theme identities and support status; no new statistic,
1185
+ method, participant, or causal claim is invented.
1186
+ """
1187
+ meta = dict(data_metadata or {})
1188
+ wanted = set(violation_types or set())
1189
+ out: list[str] = []
1190
+
1191
+ if str(mode or "").strip().lower() == "amj":
1192
+ ledger = dict(hypothesis_ledger or meta.get("hypothesis_ledger") or {})
1193
+ evidence = dict(meta.get("association_results") or {})
1194
+ if not evidence:
1195
+ evidence = dict((meta.get("quantitative_evidence_ledger") or {}).get("association_results") or {})
1196
+ relation_results = dict(evidence.get("relation_results") or {})
1197
+ indirect_results = dict(evidence.get("indirect_results") or {})
1198
+ items = [dict(x) for x in (ledger.get("items") or []) if isinstance(x, dict)]
1199
+
1200
+ def relation_sentence(item: dict) -> str:
1201
+ predictor = str(item.get("predictor") or "").strip()
1202
+ outcome = str(item.get("outcome") or "").strip()
1203
+ moderator = str(item.get("moderator") or "").strip()
1204
+ relation = str(item.get("relation") or "").strip().lower()
1205
+ row = dict(relation_results.get(str(item.get("relation_id") or "")) or {})
1206
+ if not predictor or not outcome or not row:
1207
+ return ""
1208
+ status = cls._final_abstract_ci_status(row)
1209
+ estimate = row.get("estimate")
1210
+ try:
1211
+ estimate_value = float(estimate)
1212
+ except (TypeError, ValueError):
1213
+ estimate_value = 0.0
1214
+ if relation == "moderation" and moderator:
1215
+ if status == "unsupported":
1216
+ return f"{moderator} did not significantly moderate the {predictor}–{outcome} association."
1217
+ if status == "supported":
1218
+ return f"{moderator} significantly moderated the {predictor}–{outcome} association."
1219
+ return f"The analysis assessed whether {moderator} moderated the {predictor}–{outcome} association."
1220
+ if status == "unsupported":
1221
+ return f"{predictor} was not significantly associated with {outcome}."
1222
+ direction = "positively" if estimate_value >= 0 else "negatively"
1223
+ return f"{predictor} was {direction} associated with {outcome}."
1224
+
1225
+ if wanted.intersection({
1226
+ "final_abstract_missing_unsupported_finding",
1227
+ "final_abstract_missing_moderation_finding",
1228
+ "final_abstract_missing_empirical_finding",
1229
+ }):
1230
+ unsupported = []
1231
+ moderation = []
1232
+ for item in items:
1233
+ row = dict(relation_results.get(str(item.get("relation_id") or "")) or {})
1234
+ status = cls._final_abstract_ci_status(row)
1235
+ if status == "unsupported":
1236
+ unsupported.append(item)
1237
+ if str(item.get("relation") or "").strip().lower() == "moderation":
1238
+ moderation.append(item)
1239
+ if "final_abstract_missing_unsupported_finding" in wanted:
1240
+ out.extend(filter(None, (relation_sentence(item) for item in unsupported)))
1241
+ if "final_abstract_missing_moderation_finding" in wanted and moderation:
1242
+ sentence = relation_sentence(moderation[0])
1243
+ if sentence:
1244
+ out.append(sentence)
1245
+ if "final_abstract_missing_empirical_finding" in wanted and not out:
1246
+ candidate = next((item for item in items if relation_results.get(str(item.get("relation_id") or ""))), None)
1247
+ sentence = relation_sentence(candidate or {})
1248
+ if sentence:
1249
+ out.append(sentence)
1250
+
1251
+ if "final_abstract_missing_indirect_finding" in wanted:
1252
+ indirect_rows = [dict(x) for x in (ledger.get("indirect_analyses") or []) if isinstance(x, dict)]
1253
+ selected = next((x for x in indirect_rows if x.get("numbered_hypothesis")), indirect_rows[0] if indirect_rows else None)
1254
+ if selected:
1255
+ path = [str(x or "").strip() for x in (selected.get("path") or []) if str(x or "").strip()]
1256
+ effect = dict(indirect_results.get(str(selected.get("effect_id") or "")) or {})
1257
+ if len(path) >= 3 and effect:
1258
+ status = cls._final_abstract_ci_status(effect)
1259
+ try:
1260
+ estimate_value = float(effect.get("estimate"))
1261
+ except (TypeError, ValueError):
1262
+ estimate_value = 0.0
1263
+ if status == "unsupported":
1264
+ out.append(
1265
+ f"The indirect association from {path[0]} to {path[-1]} through {path[1]} was not significant."
1266
+ )
1267
+ else:
1268
+ direction = "positive" if estimate_value >= 0 else "negative"
1269
+ out.append(
1270
+ f"The indirect association from {path[0]} to {path[-1]} through {path[1]} was {direction} and significant."
1271
+ )
1272
+
1273
+ elif str(mode or "").strip().lower() == "asq":
1274
+ if wanted.intersection({
1275
+ "final_abstract_missing_qualitative_finding",
1276
+ "final_abstract_missing_qualitative_theme_findings",
1277
+ }):
1278
+ themes = [dict(x) for x in (meta.get("themes") or []) if isinstance(x, dict)]
1279
+ labels = []
1280
+ for theme in themes:
1281
+ label = str(theme.get("title") or "").strip()
1282
+ if not label:
1283
+ concepts = [str(x or "").strip() for x in (theme.get("concepts") or []) if str(x or "").strip()]
1284
+ label = concepts[0] if concepts else ""
1285
+ if label and label not in labels:
1286
+ labels.append(label)
1287
+ if len(labels) >= 2:
1288
+ break
1289
+ if len(labels) >= 2:
1290
+ out.append(f"The analysis identified {labels[0]} and {labels[1]} as central themes.")
1291
+ elif len(labels) == 1:
1292
+ out.append(f"The analysis identified {labels[0]} as a central theme.")
1293
+
1294
+ deduped = []
1295
+ seen = set()
1296
+ for sentence in out:
1297
+ key = cls._final_abstract_normalized_text(sentence)
1298
+ if key and key not in seen:
1299
+ deduped.append(sentence)
1300
+ seen.add(key)
1301
+ return deduped
1302
+
1303
+ @classmethod
1304
+ def _final_abstract_resilience_repair(
1305
+ cls, mode: str, text: str, violations, data_metadata: dict, *, hypothesis_ledger: dict | None = None,
1306
+ ) -> tuple[str, list[dict]]:
1307
+ """Provider-neutral last-mile repair for recoverable Final-Abstract defects."""
1308
+ current = str(text or "").strip()
1309
+ rows = [dict(x) for x in (violations or []) if isinstance(x, dict)]
1310
+ kinds = {str(x.get("type") or "") for x in rows}
1311
+ diagnostics: list[dict] = []
1312
+
1313
+ metric_kinds = {"quantitative_metric_owner_unresolved", "quantitative_metric_mismatch", "unfrozen_p_value"}
1314
+ if str(mode or "").strip().lower() == "amj" and kinds.intersection(metric_kinds):
1315
+ current, metric_diagnostics = cls._final_abstract_metric_light(current)
1316
+ diagnostics.extend(metric_diagnostics)
1317
+
1318
+ finding_kinds = {
1319
+ "final_abstract_missing_empirical_finding",
1320
+ "final_abstract_missing_unsupported_finding",
1321
+ "final_abstract_missing_moderation_finding",
1322
+ "final_abstract_missing_indirect_finding",
1323
+ "final_abstract_missing_qualitative_finding",
1324
+ "final_abstract_missing_qualitative_theme_findings",
1325
+ }
1326
+ if kinds.intersection(finding_kinds):
1327
+ sentences = cls._canonical_final_abstract_findings(
1328
+ mode, data_metadata, hypothesis_ledger=hypothesis_ledger,
1329
+ violation_types=kinds.intersection(finding_kinds),
1330
+ )
1331
+ haystack = cls._final_abstract_normalized_text(current)
1332
+ added = []
1333
+ for sentence in sentences:
1334
+ key = cls._final_abstract_normalized_text(sentence)
1335
+ if key and key not in haystack:
1336
+ added.append(sentence)
1337
+ if added:
1338
+ current = (current.rstrip() + " " + " ".join(added)).strip()
1339
+ diagnostics.append({
1340
+ "type": "final_abstract_frozen_finding_fallback",
1341
+ "count": len(added),
1342
+ "reason": "Frozen finding identities were added after bounded model repair left only abstract-coverage defects.",
1343
+ })
1344
+ return current, diagnostics
1345
+
1346
  @classmethod
1347
  def _final_abstract_finding_violations(
1348
  cls, mode: str, text: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None
 
1909
  rows.extend(self._final_abstract_finding_violations(
1910
  mode, repaired, data_metadata, hypothesis_ledger=hypothesis_ledger
1911
  ))
1912
+ if rows:
1913
+ resilient, resilience_notes = self._final_abstract_resilience_repair(
1914
+ mode, repaired, rows, data_metadata, hypothesis_ledger=hypothesis_ledger
1915
+ )
1916
+ if resilient != repaired:
1917
+ repaired = resilient
1918
+ rows = self._study_fact_violations(
1919
+ mode, agent, quant_engine, repaired, data_metadata, section_name=label
1920
+ )
1921
+ if hypothesis_ledger:
1922
+ rows.extend(self._hypothesis_contract_violations(
1923
+ repaired, hypothesis_ledger, section_name=label
1924
+ ))
1925
+ rows.extend(self._final_abstract_finding_violations(
1926
+ mode, repaired, data_metadata, hypothesis_ledger=hypothesis_ledger
1927
+ ))
1928
+ if resilience_notes:
1929
+ self._record_adaptive_completion_residual(
1930
+ quality_notes, resilience_notes, section_name=label,
1931
+ stage="front_matter_resilience_recovery"
1932
+ )
1933
  current, remaining = repaired, rows
1934
  if not remaining:
1935
  return current, []
 
2500
  if 'moderation' in missing:
2501
  label = role_label.get('moderator') or ''
2502
  if code.startswith('zh'):
2503
+ suffixes.append(f'{label}的调节作用' if label else '调节作用')
2504
  elif code.startswith('ru'):
2505
+ suffixes.append(f'модерирующая роль переменной «{label}»' if label else 'модерирующая роль')
2506
  elif code.startswith('kk') or code.startswith('kz'):
2507
+ suffixes.append(f'«{label}» айнымалысының модераторлық рөлі' if label else 'модераторлық рөл')
2508
  else:
2509
+ suffixes.append(f'the moderating role of {label}' if label else 'moderation')
2510
  if missing.intersection({'mediation', 'serial_mediation', 'parallel_mediation'}):
2511
  label = role_label.get('mediator') or ''
2512
  if code.startswith('zh'):
2513
+ suffixes.append(f'{label}的中介作用' if label else '中介作用')
2514
  elif code.startswith('ru'):
2515
+ suffixes.append(f'медиирующая роль переменной «{label}»' if label else 'медиирующая роль')
2516
  elif code.startswith('kk') or code.startswith('kz'):
2517
+ suffixes.append(f'«{label}» айнымалысының медиаторлық рөлі' if label else 'медиаторлық рөл')
2518
  else:
2519
+ suffixes.append(f'the mediating role of {label}' if label else 'mediation')
2520
  if suffixes:
2521
+ separator = ':' if code.startswith('zh') else ': '
2522
+ joiner = ';' if code.startswith('zh') else '; '
2523
+ repaired_title = title.rstrip(' .;::') + separator + joiner.join(suffixes)
2524
  value = value[:title_match.start(1)] + repaired_title + value[title_match.end(1):]
2525
  diagnostics.append({'type':'target_user_relation_title_rebound','missing':sorted(missing)})
2526
  return value, diagnostics
 
5916
  remaining_final_fact_rows.extend(self._final_abstract_finding_violations(
5917
  mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger
5918
  ))
5919
+ if remaining_final_fact_rows:
5920
+ resilient_abstract, resilience_notes = self._final_abstract_resilience_repair(
5921
+ mode, final_abstract, remaining_final_fact_rows, data_metadata,
5922
+ hypothesis_ledger=hypothesis_ledger
5923
+ )
5924
+ if resilient_abstract != final_abstract:
5925
+ final_abstract = resilient_abstract
5926
+ remaining_final_fact_rows = self._study_fact_violations(
5927
+ mode, agent, quant_engine, final_abstract, data_metadata,
5928
+ section_name="Final Abstract",
5929
+ )
5930
+ if hypothesis_ledger:
5931
+ remaining_final_fact_rows.extend(self._hypothesis_contract_violations(
5932
+ final_abstract, hypothesis_ledger, section_name="Final Abstract"
5933
+ ))
5934
+ remaining_final_fact_rows.extend(self._final_abstract_finding_violations(
5935
+ mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger
5936
+ ))
5937
+ if resilience_notes:
5938
+ self._record_adaptive_completion_residual(
5939
+ quality_notes, resilience_notes, section_name="Final Abstract",
5940
+ stage="final_abstract_resilience_recovery"
5941
+ )
5942
  if remaining_final_fact_rows:
5943
  final_abstract, residual_final_rows = self._adaptive_repair_front_matter(
5944
  agent=agent, quant_engine=quant_engine, abstract_text=final_abstract,
core/quantitative_engine.py CHANGED
@@ -1861,7 +1861,7 @@ Keywords: {keywords}"""
1861
  continue
1862
  if row.get("estimate") is not None:
1863
  candidates.append((f"relation:{rid}", row.get("estimate")))
1864
- if re.search(r"(?i)indirect", clause):
1865
  for eid, row in indirect_results.items():
1866
  if not isinstance(row, dict):
1867
  continue
 
1861
  continue
1862
  if row.get("estimate") is not None:
1863
  candidates.append((f"relation:{rid}", row.get("estimate")))
1864
+ if re.search(r"(?i)\bindirect\b", clause):
1865
  for eid, row in indirect_results.items():
1866
  if not isinstance(row, dict):
1867
  continue