diff --git "a/core/generation_runner.py" "b/core/generation_runner.py" --- "a/core/generation_runner.py" +++ "b/core/generation_runner.py" @@ -28,7 +28,7 @@ from core.checkpoint_store import TaskCheckpointStore from core.meta_extractor import extract_golden_metadata from core.model_gateway import resolve_plan_policy_snapshot from core.pipeline_agent import SSciPipelineAgent, PipelineCancelledError -from core.production_guard import build_validated_package, build_recovery_package, replace_abstract +from core.production_guard import build_validated_package, replace_abstract from core.qualitative_engine import get_qualitative_engine from core.quantitative_engine import get_quantitative_engine from core.rag_engine import initialize_rag @@ -91,20 +91,6 @@ def _clean_markdown_headings(text): return re.sub(r"(?m)^#+\s+(.*)$", r"**\1**", str(text or "")) -def _regex_sub_literal(pattern, replacement, text, *, flags=0, count=0): - r"""Run regex matching while treating replacement bytes as literal text. - - Dynamic evidence/RAG payloads may legitimately contain backslashes such as - ``\h`` or LaTeX commands. Passing those strings directly as ``re.sub``'s - replacement template can raise ``re.error: bad escape``. A callable - replacement preserves the payload byte-for-byte and disables replacement - template interpretation without changing regex matching semantics. - """ - literal = str(replacement if replacement is not None else "") - source = str(text if text is not None else "") - return re.sub(pattern, lambda _match, value=literal: value, source, count=count, flags=flags) - - def _canonical_request_hash(request_data: dict[str, Any]) -> str: material = dict(request_data or {}) material.pop("request_hash", None) @@ -1036,836 +1022,6 @@ class GenerationRunner: rows.extend(scope_inspector(text, data_metadata, section_name=section_name) or []) return list(rows) - @staticmethod - def _final_abstract_normalized_text(value: str) -> str: - """Normalize reader-facing English prose for deterministic finding coverage checks.""" - return re.sub(r"[^a-z0-9]+", " ", str(value or "").casefold()).strip() - - @classmethod - def _final_abstract_identity_present(cls, sentence: str, label: str) -> bool: - needle = cls._final_abstract_normalized_text(label) - haystack = cls._final_abstract_normalized_text(sentence) - return bool(needle and haystack and needle in haystack) - - @staticmethod - def _final_abstract_result_sentences(text: str) -> list[str]: - """Return sentences that actually assert a frozen empirical finding. - - Deliberately exclude vague contribution language such as "our findings - contribute". A Final Abstract must report what the study found, not - merely say that findings exist. - """ - sentences = [ - part.strip() - for part in re.split(r"(?<=[.!?])\s+|\n+", str(text or "")) - if str(part or "").strip() - ] - finding_pattern = re.compile( - r"(?i)(?:" - r"\b(?:we|the\s+study|the\s+analysis|the\s+results?|our\s+analysis)\s+" - r"(?:find|finds|found|show|shows|showed|indicate|indicates|indicated|reveal|reveals|revealed|identify|identifies|identified|support|supports|supported)\b|" - r"\bfindings?\s+(?:show|shows|showed|indicate|indicates|indicated|reveal|reveals|revealed|identify|identifies|identified|suggest|suggests|suggested)\b|" - r"\b(?:was|were|is|are)\s+(?:positively|negatively|significantly|non[- ]?significantly|not\s+significantly)\s+(?:associated|related)\b|" - 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|" - r"\bH\d+\s+(?:was|were|is|are)\s+(?:supported|unsupported|not\s+supported|rejected)\b|" - r"\bdid\s+not\s+(?:significantly\s+)?(?:moderate|mediate|change|alter)\b" - r")" - ) - result_sentences = [] - prospective = re.compile( - r"(?i)\b(?:we\s+(?:propose|hypothesize|hypothesise|predict|expect|test)|" - r"hypothes(?:is|es|ized|ised)|this\s+study\s+(?:examines|investigates|tests)|" - r"we\s+further\s+hypothesize)\b" - ) - retrospective = re.compile( - r"(?i)\b(?:results?|findings?\s+(?:show|indicate|reveal|identify|suggest)|" - r"analysis\s+(?:show|indicate|reveal|identify)|found|revealed|indicated|" - r"supported|unsupported|non[- ]?significant|not\s+significant|nonzero|received\s+no\s+support|" - r"did\s+not\s+(?:significantly\s+)?(?:moderate|mediate|change|alter))\b" - ) - for sentence in sentences: - if not finding_pattern.search(sentence): - # Explicit frozen-status language can be result-bearing even - # without a finite 'was/were' construction. - if not re.search( - r"(?i)\b(?:nonzero\s+indirect\s+(?:association|effect)|" - r"unsupported\s+(?:interaction|moderation|association)|" - r"supported\s+(?:interaction|moderation|association))\b", - sentence, - ): - continue - if prospective.search(sentence) and not retrospective.search(sentence): - continue - result_sentences.append(sentence) - return result_sentences - - @staticmethod - def _final_abstract_ci_status(row: dict) -> str: - ci = list((row or {}).get("ci") or []) - if len(ci) != 2: - return "" - try: - low, high = float(ci[0]), float(ci[1]) - except (TypeError, ValueError): - return "" - return "supported" if (low > 0.0 or high < 0.0) else "unsupported" - - @staticmethod - def _final_abstract_support_language(sentence: str, status: str) -> bool: - value = str(sentence or "") - if status == "unsupported": - return bool(re.search( - r"(?i)\b(?:not\s+supported|unsupported|no\s+support|did\s+not\s+support|" - r"non[- ]?significant|not\s+significant|not\s+significantly\s+(?:associated|related)|" - r"no\s+(?:meaningful\s+)?(?:association|relationship|interaction|moderation|effect|evidence)|" - r"did\s+not\s+(?:significantly\s+)?(?:moderate|mediate|differ|vary|change|alter)|" - r"confidence\s+interval[^.!?]{0,80}(?:included|crossed)\s+zero)\b", - value, - )) - if status == "supported": - return bool(re.search( - r"(?i)\b(?:supported|significant|nonzero|positive(?:ly)?\s+(?:association|associated|relationship|related)|" - r"negative(?:ly)?\s+(?:association|associated|relationship|related)|" - r"was\s+associated|were\s+associated|was\s+related|were\s+related)\b", - value, - )) - return True - - @staticmethod - def _final_abstract_metric_light(text: str) -> tuple[str, list[dict]]: - """Remove nonessential inferential statistics from a Final Abstract. - - This is a completion-resilience transform, not a correctness waiver. - It is used only after a frozen Final Abstract has already been generated - and a strict study-fact validator reports a metric-specific defect. The - substantive finding sentence is preserved while beta/CI/p-value detail - that is optional in an abstract is removed. Sample-size/design facts are - intentionally untouched. - """ - value = str(text or "") - original = value - - # Parenthetical inferential bundles are the common case: - # (β = .21, 95% CI [...], p = .03) - metric_bundle = re.compile( - r"\s*\((?=[^()]{0,220}(?:β|standardized\s+estimate|(?:\d{2}%\s*)?CI\b|confidence\s+interval|\bp\s*[=<>]))[^()]*\)", - flags=re.I, - ) - value = metric_bundle.sub("", value) - - # Bare inline statistics are also optional in the abstract. Keep the - # surrounding finding language and remove only the metric surface. - value = re.sub( - r"(?i)(?:,\s*)?(?:β|standardized\s+estimate)\s*(?:=|:)?\s*[-+]?\d+(?:\.\d+)?", - "", - value, - ) - value = re.sub( - r"(?i)(?:,\s*)?(?:\d{2}%\s*)?(?:CI|confidence\s+interval)\s*[:=]?\s*[\[(][^\])]{1,80}[\])]?", - "", - value, - ) - value = re.sub(r"(?i)(?:,\s*)?\bp\s*(?:=|<|>)\s*\.?\d+(?:\.\d+)?", "", value) - - # Typography cleanup only; never alter construct identities or finding - # polarity/support language. - value = re.sub(r"\s+,", ",", value) - value = re.sub(r",\s*,+", ",", value) - value = re.sub(r"\(\s*\)", "", value) - value = re.sub(r"\s+([.;:])", r"\1", value) - value = re.sub(r" {2,}", " ", value) - value = re.sub(r"\n{3,}", "\n\n", value).strip() - - diagnostics = [] - if value != original.strip(): - diagnostics.append({ - "type": "final_abstract_metric_light_recovery", - "reason": ( - "Optional inferential statistics were removed from the Final Abstract after strict metric validation could not safely bind them." - ), - }) - return value, diagnostics - - @classmethod - def _canonical_final_abstract_findings( - cls, mode: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None, - violation_types: set[str] | None = None, - ) -> list[str]: - """Return fact-safe, metric-light finding sentences from frozen ledgers. - - These sentences are a last-resort completion fallback after DeepSeek has - already received bounded local-repair opportunities. They use only - frozen relation/theme identities and support status; no new statistic, - method, participant, or causal claim is invented. - """ - meta = dict(data_metadata or {}) - wanted = set(violation_types or set()) - out: list[str] = [] - - if str(mode or "").strip().lower() == "amj": - ledger = dict(hypothesis_ledger or meta.get("hypothesis_ledger") or {}) - evidence = dict(meta.get("association_results") or {}) - if not evidence: - evidence = dict((meta.get("quantitative_evidence_ledger") or {}).get("association_results") or {}) - relation_results = dict(evidence.get("relation_results") or {}) - indirect_results = dict(evidence.get("indirect_results") or {}) - items = [dict(x) for x in (ledger.get("items") or []) if isinstance(x, dict)] - - def relation_sentence(item: dict) -> str: - predictor = str(item.get("predictor") or "").strip() - outcome = str(item.get("outcome") or "").strip() - moderator = str(item.get("moderator") or "").strip() - relation = str(item.get("relation") or "").strip().lower() - row = dict(relation_results.get(str(item.get("relation_id") or "")) or {}) - if not predictor or not outcome or not row: - return "" - status = cls._final_abstract_ci_status(row) - estimate = row.get("estimate") - try: - estimate_value = float(estimate) - except (TypeError, ValueError): - estimate_value = 0.0 - if relation == "moderation" and moderator: - if status == "unsupported": - return f"{moderator} did not significantly moderate the {predictor}–{outcome} association." - if status == "supported": - return f"{moderator} significantly moderated the {predictor}–{outcome} association." - return f"The analysis assessed whether {moderator} moderated the {predictor}–{outcome} association." - if status == "unsupported": - return f"{predictor} was not significantly associated with {outcome}." - direction = "positively" if estimate_value >= 0 else "negatively" - return f"{predictor} was {direction} associated with {outcome}." - - if wanted.intersection({ - "final_abstract_missing_unsupported_finding", - "final_abstract_missing_moderation_finding", - "final_abstract_missing_empirical_finding", - }): - unsupported = [] - moderation = [] - for item in items: - row = dict(relation_results.get(str(item.get("relation_id") or "")) or {}) - status = cls._final_abstract_ci_status(row) - if status == "unsupported": - unsupported.append(item) - if str(item.get("relation") or "").strip().lower() == "moderation": - moderation.append(item) - if "final_abstract_missing_unsupported_finding" in wanted: - out.extend(filter(None, (relation_sentence(item) for item in unsupported))) - if "final_abstract_missing_moderation_finding" in wanted and moderation: - sentence = relation_sentence(moderation[0]) - if sentence: - out.append(sentence) - if "final_abstract_missing_empirical_finding" in wanted: - # Preserve representativeness even when the abstract already - # contains null/unsupported findings. Prefer one supported - # non-moderation relation so the fallback communicates a - # central positive/negative empirical result rather than - # becoming a list of only non-significant findings. - supported = [ - item for item in items - if str(item.get("relation") or "").strip().lower() != "moderation" - and cls._final_abstract_ci_status( - dict(relation_results.get(str(item.get("relation_id") or "")) or {}) - ) == "supported" - ] - candidate = next( - (item for item in supported if str(item.get("outcome") or "").strip()), - supported[0] if supported else None, - ) - if candidate is None and not out: - candidate = next( - (item for item in items if relation_results.get(str(item.get("relation_id") or ""))), - None, - ) - sentence = relation_sentence(candidate or {}) - if sentence: - if sentence not in out: - out.append(sentence) - - if "final_abstract_missing_indirect_finding" in wanted: - indirect_rows = [dict(x) for x in (ledger.get("indirect_analyses") or []) if isinstance(x, dict)] - selected = next((x for x in indirect_rows if x.get("numbered_hypothesis")), indirect_rows[0] if indirect_rows else None) - if selected: - path = [str(x or "").strip() for x in (selected.get("path") or []) if str(x or "").strip()] - effect = dict(indirect_results.get(str(selected.get("effect_id") or "")) or {}) - if len(path) >= 3 and effect: - status = cls._final_abstract_ci_status(effect) - try: - estimate_value = float(effect.get("estimate")) - except (TypeError, ValueError): - estimate_value = 0.0 - if status == "unsupported": - out.append( - f"The indirect association from {path[0]} to {path[-1]} through {path[1]} was not significant." - ) - else: - direction = "positive" if estimate_value >= 0 else "negative" - out.append( - f"The indirect association from {path[0]} to {path[-1]} through {path[1]} was {direction} and significant." - ) - - elif str(mode or "").strip().lower() == "asq": - if wanted.intersection({ - "final_abstract_missing_qualitative_finding", - "final_abstract_missing_qualitative_theme_findings", - }): - themes = [dict(x) for x in (meta.get("themes") or []) if isinstance(x, dict)] - labels = [] - for theme in themes: - label = str(theme.get("title") or "").strip() - if not label: - concepts = [str(x or "").strip() for x in (theme.get("concepts") or []) if str(x or "").strip()] - label = concepts[0] if concepts else "" - if label and label not in labels: - labels.append(label) - if len(labels) >= 2: - break - if len(labels) >= 2: - out.append(f"The analysis identified {labels[0]} and {labels[1]} as central themes.") - elif len(labels) == 1: - out.append(f"The analysis identified {labels[0]} as a central theme.") - - deduped = [] - seen = set() - for sentence in out: - key = cls._final_abstract_normalized_text(sentence) - if key and key not in seen: - deduped.append(sentence) - seen.add(key) - return deduped - - @classmethod - def _final_abstract_resilience_repair( - cls, mode: str, text: str, violations, data_metadata: dict, *, hypothesis_ledger: dict | None = None, - ) -> tuple[str, list[dict]]: - """Provider-neutral last-mile repair for recoverable Final-Abstract defects.""" - current = str(text or "").strip() - rows = [dict(x) for x in (violations or []) if isinstance(x, dict)] - kinds = {str(x.get("type") or "") for x in rows} - diagnostics: list[dict] = [] - - metric_kinds = {"quantitative_metric_owner_unresolved", "quantitative_metric_mismatch", "unfrozen_p_value"} - if str(mode or "").strip().lower() == "amj" and kinds.intersection(metric_kinds): - current, metric_diagnostics = cls._final_abstract_metric_light(current) - diagnostics.extend(metric_diagnostics) - - finding_kinds = { - "final_abstract_missing_empirical_finding", - "final_abstract_missing_unsupported_finding", - "final_abstract_missing_moderation_finding", - "final_abstract_missing_indirect_finding", - "final_abstract_missing_qualitative_finding", - "final_abstract_missing_qualitative_theme_findings", - } - if kinds.intersection(finding_kinds): - sentences = cls._canonical_final_abstract_findings( - mode, data_metadata, hypothesis_ledger=hypothesis_ledger, - violation_types=kinds.intersection(finding_kinds), - ) - haystack = cls._final_abstract_normalized_text(current) - added = [] - for sentence in sentences: - key = cls._final_abstract_normalized_text(sentence) - if key and key not in haystack: - added.append(sentence) - if added: - current = (current.rstrip() + " " + " ".join(added)).strip() - diagnostics.append({ - "type": "final_abstract_frozen_finding_fallback", - "count": len(added), - "reason": "Frozen finding identities were added after bounded model repair left only abstract-coverage defects.", - }) - return current, diagnostics - - @classmethod - def _deterministic_final_abstract( - cls, mode: str, topic: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None, - ) -> str: - """Build a conservative provider-free Final Abstract from frozen authority. - - Delivery-first rule: this fallback is used only after bounded model/front- - matter repair has failed. It never invents coefficients, sample sizes, - methods, or causal claims; it uses the user topic plus frozen finding/theme - ledgers already owned by the completed manuscript. - """ - clean_topic = re.sub(r"\s+", " ", str(topic or "")).strip().rstrip(".") - if str(mode or "").strip().lower() == "amj": - lead = f"This survey-based quantitative study examines {clean_topic}." if clean_topic else ( - "This survey-based quantitative study examines the associations specified in the frozen research model." - ) - method = ( - "The analysis evaluates the prespecified construct associations using the completed study evidence and reports the results conservatively." - ) - wanted = { - "final_abstract_missing_empirical_finding", - "final_abstract_missing_unsupported_finding", - "final_abstract_missing_moderation_finding", - "final_abstract_missing_indirect_finding", - } - else: - lead = f"This qualitative study examines {clean_topic}." if clean_topic else ( - "This qualitative study examines the focal management phenomenon defined by the completed manuscript." - ) - method = ( - "The analysis develops an evidence-bounded account from the completed qualitative study and preserves the study's documented design and evidence identities." - ) - wanted = { - "final_abstract_missing_qualitative_finding", - "final_abstract_missing_qualitative_theme_findings", - } - findings = cls._canonical_final_abstract_findings( - mode, data_metadata, hypothesis_ledger=hypothesis_ledger, violation_types=wanted, - ) - if not findings: - findings = [ - "The principal findings and their boundary conditions are reported directly from the completed analysis without adding unsupported numerical or causal claims." - ] - contribution = ( - "The study clarifies the focal relationships and boundary conditions in a form that remains aligned with the completed evidence and research design." - if str(mode or "").strip().lower() == "amj" - else "The study offers a structured interpretation of the focal process while keeping the claims aligned with the completed qualitative evidence." - ) - return " ".join([lead, method, *findings, contribution]).strip() - - @classmethod - def _deterministic_research_logic_contract( - cls, mode: str, topic: str, keywords: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None, - ) -> str: - """Provider-free planning contract used when research-logic generation fails. - - This is an internal planning anchor, not reader-facing manuscript prose. It - intentionally stays conservative: no result direction, coefficient, sample - value, causal claim, or bibliographic assertion is introduced. Quantitative - H-identities are copied only from the frozen ledger. - """ - clean_topic = re.sub(r"\s+", " ", str(topic or "")).strip() - clean_keywords = ", ".join(parse_keyword_terms(keywords, explicit_user=True)) - if str(mode or "").strip().casefold() == "amj": - parts = [ - f"Research focus: {clean_topic}." if clean_topic else "Research focus: the management problem defined by the frozen user request.", - "Design: survey-based quantitative research.", - "Planning rule: develop the management problem and theoretical rationale without predicting coefficients, significance, support, or causal effects.", - "Evidence rule: use only the documented study design and completed analysis as authority for empirical facts and results.", - ] - items = [item for item in (dict(hypothesis_ledger or {}).get("items") or []) if isinstance(item, dict)] - for item in items: - hid = re.sub(r"\s+", " ", str(item.get("hypothesis_id") or "")).strip().upper() - predictor = re.sub(r"\s+", " ", str(item.get("predictor") or "")).strip() - outcome = re.sub(r"\s+", " ", str(item.get("outcome") or "")).strip() - moderator = re.sub(r"\s+", " ", str(item.get("moderator") or "")).strip() - relation = str(item.get("relation") or "association").strip().casefold() - if not hid or not predictor or not outcome: - continue - if relation == "moderation" and moderator: - parts.append( - f"{hid} defines {moderator} as the moderator of the association between {predictor} and {outcome}." - ) - else: - parts.append(f"{hid} defines the association between {predictor} and {outcome}.") - else: - parts = [ - f"Research focus: {clean_topic}." if clean_topic else "Research focus: the management phenomenon defined by the frozen user request.", - "Design: qualitative research.", - "Planning rule: examine the organizational process, meanings, mechanisms, and boundary conditions without predicting an emergent resolution.", - "Evidence rule: use only the documented qualitative setting, data sources, and completed evidence; do not invent participants, quotations, counts, or findings.", - ] - if clean_keywords: - parts.insert(1, f"User keywords: {clean_keywords}.") - return " ".join(part for part in parts if str(part or "").strip()).strip() - - @classmethod - def _deterministic_working_front_matter( - cls, mode: str, topic: str, keywords: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None, - ) -> dict: - """Build provisional front matter without a provider call. - - Working front matter is only a continuity anchor before body drafting. The - fallback therefore avoids empirical findings entirely and prefers frozen - user keyword authority. Final front matter remains independently rebuilt - from the completed manuscript later. - """ - clean_topic = re.sub(r"\s+", " ", str(topic or "")).strip().rstrip(".") - candidate_keywords = parse_keyword_terms(keywords, explicit_user=True) - resolved_keywords = cls._canonical_quantitative_front_matter_keywords( - mode, candidate_keywords, data_metadata - ) - if not resolved_keywords: - resolved_keywords = cls._front_matter_keyword_fallback_terms(data_metadata) - if not resolved_keywords: - resolved_keywords = ["Management Research"] - - if str(mode or "").strip().casefold() == "amj": - lead = ( - f"This survey-based quantitative study examines {clean_topic}." - if clean_topic else - "This survey-based quantitative study examines the management relationships defined by the user request." - ) - abstract = " ".join([ - lead, - "The manuscript develops the theoretical rationale for the prespecified research model and evaluates the documented associations using the completed study evidence.", - "At this planning stage, no coefficient, significance test, hypothesis support status, or causal conclusion is anticipated.", - "The final abstract will be reconstructed from the completed analysis and frozen evidence before delivery.", - ]) - else: - lead = ( - f"This qualitative study examines {clean_topic}." - if clean_topic else - "This qualitative study examines the management phenomenon defined by the user request." - ) - abstract = " ".join([ - lead, - "The manuscript uses the documented qualitative study design to examine the focal process, meanings, mechanisms, and boundary conditions.", - "At this planning stage, no theme, quotation, participant detail, or substantive finding is anticipated beyond the frozen study design.", - "The final abstract will be reconstructed from the completed qualitative evidence before delivery.", - ]) - return {"abstract": abstract.strip(), "keywords": list(resolved_keywords)} - - @classmethod - def _deterministic_hypothesis_reconcile(cls, text: str, ledger: dict) -> tuple[str, list[dict]]: - """Reconcile H-identities without model rewriting or result-driven remapping. - - Unsafe H-labelled sentences are removed rather than semantically rewritten; - missing canonical identities are then appended from the frozen ledger. This - keeps P1 identity integrity while avoiding a task kill for mechanically - repairable H-label defects. - """ - value = cls._normalize_hypothesis_ordinal_aliases(str(text or ""), ledger) - rows = cls._hypothesis_contract_violations(value, ledger, section_name="Full Manuscript", require_all=True) - diagnostics: list[dict] = [] - if not rows: - return value, diagnostics - - # Remove whole sentences whose H label is unknown or attached to the wrong - # construct pairing. Violation previews are intentionally capped, so - # match them as normalized prefixes against the original sentence spans - # rather than deleting only the truncated preview and leaving a prose tail. - bad_sentence_previews = [] - unknown_ids = set() - for row in rows: - kind = str(row.get("type") or "") - if kind == "hypothesis_contract_pairing_mismatch" and str(row.get("sentence") or "").strip(): - bad_sentence_previews.append(re.sub(r"\s+", " ", str(row.get("sentence") or "")).strip()) - if kind == "hypothesis_contract_unknown_id": - unknown_ids.update(str(x or "").upper() for x in (row.get("observed") or []) if str(x or "").strip()) - - spans_to_remove = [] - for start, end, sentence in SSciPipelineAgent._sentence_spans(value): - normalized_sentence = re.sub(r"\s+", " ", str(sentence or "")).strip() - pairing_match = any( - normalized_sentence == preview - or normalized_sentence.startswith(preview) - or preview.startswith(normalized_sentence) - for preview in bad_sentence_previews if preview - ) - unknown_match = bool(unknown_ids) and any( - re.search(rf"\b{re.escape(hid)}\b", sentence, flags=re.I) for hid in unknown_ids - ) - if pairing_match or unknown_match: - spans_to_remove.append((start, end, pairing_match, unknown_match)) - - removed_pairing = 0 - removed_unknown = 0 - for start, end, pairing_match, unknown_match in reversed(spans_to_remove): - value = value[:start] + value[end:] - removed_pairing += int(pairing_match) - removed_unknown += int(unknown_match) - if removed_pairing: - diagnostics.append({ - "type": "hypothesis_pairing_sentence_removed", - "count": removed_pairing, - "reason": "Sentence(s) attaching a frozen H-identifier to the wrong construct pairing were removed before deterministic H-identity reconstruction.", - }) - if removed_unknown: - diagnostics.append({ - "type": "unknown_hypothesis_sentence_removed", - "count": removed_unknown, - "reason": "Sentence(s) containing H-identifiers outside the frozen hypothesis ledger were removed before deterministic H-identity reconstruction.", - }) - - # A residual unknown token can still exist in a heading/range that the - # sentence scanner did not classify as prose. Remove the identifier only - # as a final mechanical cleanup; substantive H-content is never rewritten. - if unknown_ids: - pattern = r"\b(?:" + "|".join(re.escape(x) for x in sorted(unknown_ids, key=len, reverse=True)) + r")\b" - value, count = re.subn(pattern, "", value, flags=re.I) - if count: - diagnostics.append({ - "type": "unknown_hypothesis_identifier_removed", - "count": count, - "reason": "Residual unknown H-identifiers were removed; only the frozen hypothesis ledger may own H-label identity.", - }) - - value = re.sub(r"[ \t]{2,}", " ", value) - value = re.sub(r"\n{3,}", "\n\n", value).strip() - remaining = cls._hypothesis_contract_violations(value, ledger, section_name="Full Manuscript", require_all=True) - missing = [] - for row in remaining: - if str(row.get("type") or "") == "hypothesis_contract_missing_id": - missing.extend(str(x or "").upper() for x in (row.get("expected") or []) if str(x or "").strip()) - observed = {str(x or "").upper() for x in (row.get("observed") or []) if str(x or "").strip()} - missing = [x for x in missing if x not in observed] - if missing: - items = { - str(item.get("hypothesis_id") or "").strip().upper(): item - for item in (dict(ledger or {}).get("items") or []) if isinstance(item, dict) - } - lines = [] - for hid in dict.fromkeys(missing): - item = dict(items.get(hid) or {}) - predictor = re.sub(r"\s+", " ", str(item.get("predictor") or "")).strip() - outcome = re.sub(r"\s+", " ", str(item.get("outcome") or "")).strip() - moderator = re.sub(r"\s+", " ", str(item.get("moderator") or "")).strip() - relation = str(item.get("relation") or "association").strip().lower() - if not predictor or not outcome: - continue - if relation == "moderation" and moderator: - lines.append(f"{hid} states that {moderator} moderates the association between {predictor} and {outcome}.") - else: - lines.append(f"{hid} states that {predictor} is associated with {outcome}.") - if lines: - paragraph = "Hypothesis identity summary: " + " ".join(lines) - # Put the deterministic summary at the end of the theory section - # when that structural boundary is available; otherwise append to - # the frozen body. No new empirical result is introduced. - match = re.search(r"(?m)^#\s+(?:Methodology|Methods|Research Setting and Methods)\s*$", value) - if match: - value = value[:match.start()].rstrip() + "\n\n" + paragraph + "\n\n" + value[match.start():].lstrip() - else: - value = value.rstrip() + "\n\n" + paragraph - diagnostics.append({ - "type": "missing_hypothesis_identity_reconstructed", - "count": len(lines), - "reason": "Missing H-identifiers were reconstructed only from the frozen hypothesis ledger.", - }) - return value, diagnostics - - @classmethod - def _final_abstract_finding_violations( - cls, mode: str, text: str, data_metadata: dict, *, hypothesis_ledger: dict | None = None - ) -> list[dict]: - """S1-R2 deterministic Final-Abstract frozen-finding coverage gate. - - The gate is language-neutral at the manuscript-authority layer: English - Final Front Matter is frozen once, then ZH/RU/KK are semantic - translations of that exact source. Product tier is intentionally not an - input because Standard and Flagship share this correctness contract. - """ - value = str(text or "").strip() - meta = dict(data_metadata or {}) - result_sentences = cls._final_abstract_result_sentences(value) - rows: list[dict] = [] - - if str(mode or "").strip().lower() == "amj": - ledger = dict(hypothesis_ledger or meta.get("hypothesis_ledger") or {}) - evidence = dict(meta.get("association_results") or {}) - if not evidence: - evidence = dict((meta.get("quantitative_evidence_ledger") or {}).get("association_results") or {}) - relation_results = dict(evidence.get("relation_results") or {}) - indirect_results = dict(evidence.get("indirect_results") or {}) - - def relation_sentence(item: dict, *, require_status: bool = True) -> bool: - relation_id = str(item.get("relation_id") or "") - frozen = dict(relation_results.get(relation_id) or {}) - status = cls._final_abstract_ci_status(frozen) - identities = [item.get("predictor"), item.get("outcome")] - if str(item.get("relation") or "") == "moderation": - identities.append(item.get("moderator")) - for sentence in result_sentences: - if not all(cls._final_abstract_identity_present(sentence, label) for label in identities if label): - continue - if str(item.get("relation") or "") == "moderation" and not re.search(r"(?i)\b(?:moderat|interaction)\w*\b", sentence): - continue - if require_status and status and not cls._final_abstract_support_language(sentence, status): - continue - return True - return False - - numbered = [dict(item) for item in (ledger.get("items") or []) if isinstance(item, dict)] - unsupported = [] - moderation = [] - for item in numbered: - frozen = dict(relation_results.get(str(item.get("relation_id") or "")) or {}) - status = cls._final_abstract_ci_status(frozen) - if status == "unsupported": - unsupported.append(item) - if str(item.get("relation") or "") == "moderation": - moderation.append(item) - - # Unsupported numbered hypotheses are interpretation-critical and - # must never disappear from the Final Abstract. - for item in unsupported: - if relation_sentence(item, require_status=True): - continue - rows.append({ - "type": "final_abstract_missing_unsupported_finding", - "hypothesis_id": str(item.get("hypothesis_id") or ""), - "reason": ( - "The Final Abstract omits the frozen unsupported/non-significant status of a numbered hypothesis." - ), - "repair_instruction": ( - "State the unsupported finding concisely in ordinary results language, preserving the exact construct identities. " - "Do not add coefficients unless indispensable." - ), - }) - - # A focal moderation result is a main boundary-condition finding even - # when supported; require one moderation result sentence when present. - if moderation and not any(relation_sentence(item, require_status=True) for item in moderation): - item = moderation[0] - rows.append({ - "type": "final_abstract_missing_moderation_finding", - "hypothesis_id": str(item.get("hypothesis_id") or ""), - "reason": "The Final Abstract does not report the frozen moderation/interaction finding.", - "repair_instruction": ( - "Report whether the frozen moderator changed the focal association, using the exact predictor, moderator, and outcome identities. " - "Keep the statement concise and evidence-bounded." - ), - }) - - # Preserve the main mechanism finding when an indirect analysis was - # actually compiled. If several unnumbered paths exist, one main - # frozen path is sufficient; numbered indirect hypotheses remain - # independently governed by the hypothesis contract. - indirect_rows = [dict(item) for item in (ledger.get("indirect_analyses") or []) if isinstance(item, dict)] - if indirect_rows: - selected = next((item for item in indirect_rows if item.get("numbered_hypothesis")), indirect_rows[0]) - effect = dict(indirect_results.get(str(selected.get("effect_id") or "")) or {}) - status = cls._final_abstract_ci_status(effect) - path = list(selected.get("path") or []) - indirect_found = False - for sentence in result_sentences: - if not re.search(r"(?i)\b(?:indirect|mediat)\w*\b", sentence): - continue - required_labels = [] - if path: - required_labels.extend([path[0], path[-1]]) - if len(path) > 2: - required_labels.append(path[1]) - if required_labels and not all(cls._final_abstract_identity_present(sentence, x) for x in required_labels if x): - continue - if status and not cls._final_abstract_support_language(sentence, status): - continue - indirect_found = True - break - if not indirect_found: - rows.append({ - "type": "final_abstract_missing_indirect_finding", - "reason": "The Final Abstract omits the main frozen indirect/mediation finding.", - "repair_instruction": ( - "Summarize the frozen indirect association through the identified mediator in one concise findings sentence. " - "Do not claim causal mediation for observational data and do not add unnecessary statistics." - ), - }) - - # Every empirical Final Abstract needs at least one actual result - # sentence. Design/hypothesis prose plus 'our findings contribute' - # is not sufficient. - if relation_results and not result_sentences: - rows.append({ - "type": "final_abstract_missing_empirical_finding", - "reason": "The Final Abstract describes the design/hypotheses but states no frozen empirical result.", - "repair_instruction": ( - "Add one or two concise sentences stating the main frozen empirical findings before the contribution. " - "Prefer support/non-support and substantive direction over a list of coefficients." - ), - }) - return rows - - if str(mode or "").strip().lower() == "asq": - themes = [dict(item) for item in (meta.get("themes") or []) if isinstance(item, dict)] - if not themes: - if not result_sentences: - rows.append({ - "type": "final_abstract_missing_qualitative_finding", - "reason": "The Final Abstract states no empirical qualitative finding.", - "repair_instruction": "Add a concise findings sentence grounded in the frozen qualitative evidence; do not invent a theme or process claim.", - }) - return rows - - covered = set() - for index, theme in enumerate(themes): - anchors = [str(theme.get("title") or "").strip()] - anchors.extend(str(x or "").strip() for x in (theme.get("concepts") or [])) - anchors = [x for x in anchors if x] - for sentence in result_sentences: - if any(cls._final_abstract_identity_present(sentence, anchor) for anchor in anchors): - covered.add(index) - break - required = min(2, len(themes)) - if len(covered) < required: - rows.append({ - "type": "final_abstract_missing_qualitative_theme_findings", - "observed": len(covered), - "expected": required, - "reason": ( - "The Final Abstract does not report enough frozen qualitative theme/concept findings to function as a results-bearing final abstract." - ), - "repair_instruction": ( - f"State the main qualitative findings in one or two concise sentences grounded in at least {required} frozen theme/concept identities. " - "Do not merely say that findings contribute, and do not invent themes, quotations, participants, or procedures." - ), - }) - return rows - - return rows - - def _delivery_first_final_abstract_language_guard( - self, *, mode: str, agent, quant_engine, final_abstract: str, reader_topic_en: str, - final_title: str, data_metadata: dict, hypothesis_ledger: dict | None, quality_notes: list, - ) -> tuple[str, bool]: - """Repair residual non-English script without turning a reader-surface defect into P0 failure. - - Returns ``(abstract, rebuilt)``. Explicit P1 integrity validators remain - authoritative; only the language-surface defect is downgraded when a - provider-neutral clean reconstruction cannot be proven safe. - """ - current = str(final_abstract or "").strip() - if not agent._english_reader_surface_has_foreign_script(current): - return current, False - - language_fallback = self._deterministic_final_abstract( - mode, reader_topic_en or final_title, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - language_rows = self._study_fact_violations( - mode, agent, quant_engine, language_fallback, data_metadata, section_name="Final Abstract" - ) - if hypothesis_ledger: - language_rows.extend(self._hypothesis_contract_violations( - language_fallback, hypothesis_ledger, section_name="Final Abstract" - )) - language_rows.extend(self._final_abstract_finding_violations( - mode, language_fallback, data_metadata, hypothesis_ledger=hypothesis_ledger - )) - fatal_language_rows, advisory_language_rows = self._partition_section_violations( - language_rows, section_name="Final Abstract", mode=mode, exhausted=True - ) - if ( - language_fallback - and not agent._english_reader_surface_has_foreign_script(language_fallback) - and not fatal_language_rows - ): - self._record_adaptive_completion_residual( - quality_notes, - advisory_language_rows + [{ - "type": "final_english_front_matter_language_fallback", - "reason": "Residual non-English script in the Final Abstract was replaced by a deterministic English reconstruction from frozen research authority." - }], - section_name="Final Abstract", - stage="delivery_first_front_matter_language_fallback", - ) - return language_fallback, True - - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "final_english_front_matter_language_warning", - "reason": "Final Abstract still contains non-English script after deterministic language fallback; delivery was preserved under the P0 Delivery-First contract." - }], - section_name="Final Abstract", - stage="delivery_first_front_matter_language_warning", - ) - return current, False - @staticmethod def _section_retry_feedback(violations, *, limit=8) -> str: rows = [] @@ -1902,6 +1058,21 @@ class GenerationRunner: rows.append(' | '.join(parts)) return '\n'.join(f'- {row}' for row in rows) + @staticmethod + def _section_forbids_external_citations(section_title: str = "", section_id: str = "") -> bool: + """One authority for manuscript units that must remain citation-free. + + P3-A4 closes a historic split where Results/Findings were declared + citation-free in the Writer prompt but did not enter the same quarantine + validation path as Conclusion/Appendix-style units. + """ + title = str(section_title or '').casefold() + sid = str(section_id or '').strip().casefold() + return ( + any(word in title for word in ("abstract", "keyword", "conclusion", "references", "appendix")) + or sid in {"results", "findings"} + ) + @staticmethod def _section_violation_severity(row, *, section_name: str = "", mode: str = "", exhausted: bool = False) -> str: """Map one validation issue to REPAIRABLE/FATAL/ADVISORY. @@ -1929,17 +1100,16 @@ class GenerationRunner: "unsupported_study_procedure", "enumeration_contract_invalid", "hypothesis_contract_missing_id", "hypothesis_contract_ordinal_alias", "hypothesis_contract_pairing_mismatch", "hypothesis_contract_unknown_id", - "citation_outside_frozen_section_whitelist", "authorial_ai_meta_trace", + "citation_outside_frozen_section_whitelist", + "citation_resolved_marker_outside_frozen_section_whitelist", + "citation_unknown_tag", "citation_resolved_marker_without_authority", + "citation_resolved_marker_ambiguous", "citation_noncanonical_resolved_marker", + "citation_forbidden_in_quarantined_unit", + "authorial_ai_meta_trace", "study_design_scope_mismatch", "unsupported_focal_construct", "unsupported_relation_claim", "unsupported_moderation_claim", "unsupported_mediation_claim", "unsupported_temporal_scope", "unsupported_design_tradition", "unsupported_unit_of_analysis", "unsupported_data_source_claim", "unsupported_process_claim", - "final_abstract_missing_empirical_finding", - "final_abstract_missing_unsupported_finding", - "final_abstract_missing_moderation_finding", - "final_abstract_missing_indirect_finding", - "final_abstract_missing_qualitative_finding", - "final_abstract_missing_qualitative_theme_findings", } if kind in always_fatal: return "FATAL" @@ -1954,11 +1124,10 @@ class GenerationRunner: )) return "FATAL" if core_citation_section else "ADVISORY" - # Delivery-First hardening: unknown/new validators are not allowed to - # create an accidental terminal path merely because a developer forgot - # to register their severity. Only explicit P1/hard-failure identities - # above may remain fatal after bounded repair. - return "ADVISORY" + # Unknown validation types remain fail-closed. This keeps C5.5 from + # silently downgrading a future integrity contract merely because it is + # new to the severity registry. + return "FATAL" @staticmethod def _section_violation_lineage(row) -> str: @@ -1972,7 +1141,17 @@ class GenerationRunner: item = dict(row or {}) if isinstance(row, dict) else {"type": "unknown_validation_issue"} kind = str(item.get("type") or "unknown_validation_issue") groups = { - "citation_scope": {"citation_outside_frozen_section_whitelist"}, + "citation_scope": { + "citation_outside_frozen_section_whitelist", + "citation_resolved_marker_outside_frozen_section_whitelist", + }, + "citation_authority": { + "citation_unknown_tag", + "citation_resolved_marker_without_authority", + "citation_resolved_marker_ambiguous", + "citation_noncanonical_resolved_marker", + "citation_forbidden_in_quarantined_unit", + }, "citation_ownership": {"citation_evidence_ownership_mismatch"}, "citation_support": {"citation_unsupported_claim", "citation_support_unverified"}, "citation_compound": {"citation_claim_split_required"}, @@ -2067,25 +1246,6 @@ class GenerationRunner: blocking.append(row) return blocking, advisory - @classmethod - def _raise_if_fatal_completion_residual( - cls, rows, *, section_name: str = "", mode: str = "", error_code: str = "final_validation_unresolved" - ) -> None: - """Fail closed only for residuals already classified as FATAL. - - Adaptive completion may retain advisory diagnostics, but it must never - downgrade hypothesis identity or other fatal correctness contracts into - a shippable manuscript. - """ - fatal = [ - row for row in list(rows or []) - if cls._section_violation_severity( - row, section_name=section_name, mode=mode, exhausted=True - ) == "FATAL" - ] - if fatal: - raise RuntimeError(f"{error_code}:{fatal[:4]}") - @classmethod def _partition_section_violations_by_lineage( cls, rows, lineage_attempts, *, section_name: str = "", mode: str = "", @@ -2110,6 +1270,20 @@ class GenerationRunner: blocking.append(row) return blocking, advisory + @classmethod + def _persistent_citation_authority_rows(cls, rows) -> list[dict]: + """Return citation identity/scope defects that may never cross a Section seal. + + P3-A4 intentionally keeps this narrower than general validation failure. + Citation support/style advisories retain their existing adaptive-completion + behavior; only source identity and unit-scope authority stay fail-closed. + """ + return [ + row for row in list(rows or []) + if isinstance(row, dict) + and cls._section_violation_lineage(row) in {"citation_scope", "citation_authority"} + ] + @staticmethod def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None: """Append reader-facing, nonfatal notes without exposing control vocabulary.""" @@ -2177,6 +1351,7 @@ class GenerationRunner: self, *, agent, quant_engine, text: str, violations, mode: str, section_id: str, section_name: str, data_metadata: dict, study_evidence: str, evidence_contract: str, hypothesis_ledger: dict, task: dict, quote_invocation_plan: dict, quality_notes: list, + allowed_citation_tags=None, quarantine=False, ): """A2 last-mile semantic repair after lineage retry exhaustion. @@ -2215,10 +1390,22 @@ class GenerationRunner: repaired, _enum_rows = agent.repair_and_validate_enumeration_contract( repaired, section_name=section_name ) + # P3-A4: every model-owned last-mile rewrite must re-enter the same + # citation authority + claim-specific semantic Gate before it can be + # considered sealable. Otherwise an unrelated adaptive repair could + # reintroduce a pre-resolved/unknown/wrong-scope citation after the + # earlier Section citation audit had already passed. + repaired = agent.audit_and_resolve_citations( + repaired, section_name=section_name, section_id=section_id, + quarantine=quarantine, allowed_tags=allowed_citation_tags, + ) + adaptive_citation_rows = copy.deepcopy( + list(getattr(agent, "_last_citation_semantic_violations", []) or []) + ) rows = self._section_validation_bundle( mode, agent, quant_engine, repaired, data_metadata, section_id=section_id, section_name=section_name, evidence_contract=evidence_contract, include_critic=False, - hypothesis_ledger=hypothesis_ledger, + citation_violations=adaptive_citation_rows, hypothesis_ledger=hypothesis_ledger, ) blocking, advisory = self._partition_section_violations( rows, section_name=section_name, mode=mode, exhausted=True @@ -2259,31 +1446,6 @@ class GenerationRunner: rows.extend(self._hypothesis_contract_violations( repaired, hypothesis_ledger, section_name=label )) - if str(label or "").strip().casefold() == "final abstract": - rows.extend(self._final_abstract_finding_violations( - mode, repaired, data_metadata, hypothesis_ledger=hypothesis_ledger - )) - if rows: - resilient, resilience_notes = self._final_abstract_resilience_repair( - mode, repaired, rows, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - if resilient != repaired: - repaired = resilient - rows = self._study_fact_violations( - mode, agent, quant_engine, repaired, data_metadata, section_name=label - ) - if hypothesis_ledger: - rows.extend(self._hypothesis_contract_violations( - repaired, hypothesis_ledger, section_name=label - )) - rows.extend(self._final_abstract_finding_violations( - mode, repaired, data_metadata, hypothesis_ledger=hypothesis_ledger - )) - if resilience_notes: - self._record_adaptive_completion_residual( - quality_notes, resilience_notes, section_name=label, - stage="front_matter_resilience_recovery" - ) current, remaining = repaired, rows if not remaining: return current, [] @@ -2653,55 +1815,6 @@ class GenerationRunner: "sha256": cls._section_integrity_hash(section_text), } - def _refresh_section_integrity_seals_after_delivery_repair( - self, manuscript: str, section_order, section_integrity_seals: dict, - ) -> list[tuple[str, str, str]]: - """Reseal only body sections changed by a deterministic P0 repair. - - G1 permits a narrow deterministic hypothesis-identity repair after the - ordinary section seals have been created. Any changed section must be - resealed immediately so resume/checkpoint authority never points at stale - bytes. The returned section text also lets the caller invalidate/requeue - target translation from the exact repaired English source. - """ - changed: list[tuple[str, str, str]] = [] - seals = section_integrity_seals - for section_key, section_title in list(section_order or []): - section_text = self._extract_top_level_section(manuscript, section_title) - if not section_text: - raise RuntimeError(f"delivery_first_reseal_source_missing:{section_title}") - current = dict(seals.get(section_key) or {}) - updated = self._build_section_integrity_seal(section_key, section_title, section_text) - if str(current.get("sha256") or "") != str(updated.get("sha256") or ""): - seals[section_key] = updated - changed.append((str(section_key), str(section_title), section_text)) - return changed - - def _requeue_resealed_translation_sections( - self, *, changed_sections, mode: str, quant_engine, data_metadata: dict, - translation_agent, translation_sections: dict, english_commit_text: str, - ) -> int: - """Requeue translations whose frozen English source changed after repair.""" - queued = 0 - for section_key, section_title, section_text in list(changed_sections or []): - source_text = section_text - if ( - str(mode or "").strip().casefold() == "amj" - and str(section_key).strip().casefold() == "results" - and dict(data_metadata or {}).get("quantitative_table_manifest") - ): - source_text = quant_engine._strip_target_quantitative_markdown_tables(section_text) - if self._enqueue_translation_unit( - translation_agent, - unit_key=section_key, - unit_name=section_title, - source_text=source_text, - translation_sections=translation_sections, - english_commit_text=english_commit_text, - ): - queued += 1 - return queued - @staticmethod def _replace_top_level_section(manuscript: str, title: str, replacement: str) -> str: value = str(manuscript or "") @@ -2744,73 +1857,6 @@ class GenerationRunner: """ return str(topic or "").strip(), str(keywords or "").strip() - @staticmethod - def _front_matter_language_projection_hash(topic: str, keywords) -> str: - terms = parse_keyword_terms(keywords, explicit_user=True) - payload = json.dumps( - {"title": str(topic or "").strip(), "keywords": terms}, - ensure_ascii=False, sort_keys=True, separators=(",", ":"), - ).encode("utf-8") - return hashlib.sha256(payload).hexdigest() - - def _resolve_english_front_matter_projection( - self, agent, state: dict, data_metadata: dict, source_topic: str, source_keywords - ) -> dict: - """Resolve/cache a language-only English projection without mutating user intent.""" - meta = dict(data_metadata or {}) - contract = dict(meta.get("study_design_contract") or meta.get("front_matter_contract") or {}) - intent = dict(contract.get("user_research_intent") or meta.get("user_research_intent") or {}) - semantic_title = str(intent.get("source_topic") or source_topic or "").strip() - semantic_keywords = list(intent.get("source_keywords") or parse_keyword_terms(source_keywords, explicit_user=True)) - source_hash = self._front_matter_language_projection_hash(semantic_title, semantic_keywords) - expected_count = len(semantic_keywords) - - cached = dict( - state.get("front_matter_language_projection") - or meta.get("front_matter_language_projection") - or {} - ) - if ( - cached.get("version") == "front-matter-language-authority-v1" - and cached.get("source_hash") == source_hash - ): - try: - validated = agent.validate_reader_facing_english_projection( - cached, expected_keyword_count=expected_count - ) - return { - "version": "front-matter-language-authority-v1", - "source_hash": source_hash, - "english_title": validated["title"], - "english_keywords": list(validated["keywords"]), - "translation_applied": bool(cached.get("translation_applied")), - } - except Exception as exc: - logging.warning( - "Cached English front-matter language projection invalid; rebuilding | error=%s", - type(exc).__name__, - ) - - resolved = agent.normalize_reader_facing_front_matter_to_english( - semantic_title, semantic_keywords - ) - validated = agent.validate_reader_facing_english_projection( - resolved, expected_keyword_count=expected_count - ) - projection = { - "version": "front-matter-language-authority-v1", - "source_hash": source_hash, - "english_title": validated["title"], - "english_keywords": list(validated["keywords"]), - "translation_applied": bool(resolved.get("translation_applied")), - } - # Presentation projection is additive metadata only. Original source_topic - # and source_keywords inside user_research_intent remain byte-for-byte authority. - data_metadata["front_matter_language_projection"] = dict(projection) - state["front_matter_language_projection"] = dict(projection) - state["data_metadata"] = data_metadata - return projection - @staticmethod def _resolved_front_matter_keyword_policy(data_metadata: dict): """Resolve one policy object for Working/Final/retry/resume/fallback paths.""" @@ -2822,12 +1868,8 @@ class GenerationRunner: @staticmethod def _front_matter_keyword_fallback_terms(data_metadata: dict): - """Derive recoverable English reader keywords from frozen study-design authority.""" + """Derive recoverable generated keywords only from frozen study-design authority.""" meta = dict(data_metadata or {}) - projection = dict(meta.get("front_matter_language_projection") or {}) - projected = [str(item).strip() for item in (projection.get("english_keywords") or []) if str(item).strip()] - if projected: - return projected contract = dict(meta.get("study_design_contract") or {}) values = [] quant = dict(contract.get("quantitative") or {}) @@ -2861,12 +1903,10 @@ class GenerationRunner: policy = keyword_policy_from_contract(contract) if str(policy.get("authority_source") or "").upper() != "USER_EXPLICIT": return [] - projection = dict(meta.get("front_matter_language_projection") or {}) - authoritative = list(projection.get("english_keywords") or intent.get("source_keywords") or []) return apply_keyword_policy( - authoritative, + intent.get("source_keywords") or [], policy, - authoritative=authoritative, + authoritative=intent.get("source_keywords") or [], ) @staticmethod @@ -2876,8 +1916,7 @@ class GenerationRunner: contract = dict(meta.get("study_design_contract") or meta.get("front_matter_contract") or {}) intent = dict(contract.get("user_research_intent") or meta.get("user_research_intent") or {}) policy = keyword_policy_from_contract(contract) - projection = dict(meta.get("front_matter_language_projection") or {}) - authoritative = list(projection.get("english_keywords") or intent.get("source_keywords") or []) + authoritative = list(intent.get("source_keywords") or []) values = apply_keyword_policy( candidate_keywords, policy, @@ -2892,13 +1931,7 @@ class GenerationRunner: rendered = re.sub(r"\s+", " ", str(title or "")).strip() if not rendered: return value - title_replacement = f"# {rendered}" - updated, count = re.subn( - r"(?m)^#\s+.+$", - lambda _match, replacement=title_replacement: replacement, - value, - count=1, - ) + updated, count = re.subn(r"(?m)^#\s+.+$", f"# {rendered}", value, count=1) if count == 1: return updated logging.warning("Manuscript title heading missing during final reconciliation; reconstructing canonical H1 instead of failing generation.") @@ -2929,26 +1962,13 @@ class GenerationRunner: @staticmethod - def _target_front_matter_language_code(target_lang: str) -> str: - value = str(target_lang or '').strip().casefold().replace('_', '-') - if value.startswith('zh') or 'chinese' in value or '中文' in value or '简体中文' in value: - return 'zh' - if value.startswith('ru') or 'russian' in value or 'рус' in value: - return 'ru' - if value.startswith('kk') or value.startswith('kz') or 'kazakh' in value or 'қазақ' in value: - return 'kk' - if value.startswith('en') or 'english' in value: - return 'en' - return value - - @classmethod - def _target_keyword_heading_aliases(cls, target_lang: str) -> list[str]: - code = cls._target_front_matter_language_code(target_lang) - if code == 'zh': + def _target_keyword_heading_aliases(target_lang: str) -> list[str]: + code = str(target_lang or '').strip().casefold().replace('_', '-') + if code.startswith('zh'): return ['关键词', '关键字'] - if code == 'ru': + if code.startswith('ru'): return ['Ключевые слова'] - if code == 'kk': + if code.startswith('kk') or code.startswith('kz'): return ['Түйін сөздер', 'Кілт сөздер'] return ['Keywords'] @@ -2959,13 +1979,7 @@ class GenerationRunner: meta = dict(data_metadata or {}) contract = dict(meta.get('study_design_contract') or {}) intent = dict(contract.get('user_research_intent') or meta.get('user_research_intent') or {}) - projection = dict(meta.get('front_matter_language_projection') or {}) - source_keywords = list( - projection.get('english_keywords') - or intent.get('source_keywords') - or contract.get('source_keywords') - or [] - ) + source_keywords = list(intent.get('source_keywords') or contract.get('source_keywords') or []) mapping = dict(glossary or {}) diagnostics = [] @@ -2997,32 +2011,30 @@ class GenerationRunner: values = [str(row.get('label') or '') for row in construct_rows if role in (row.get('roles') or []) and row.get('label')] if len(values) == 1: role_label[role] = re.sub(r'\s+', ' ', str(mapping.get(values[0]) or values[0])).strip() - code = cls._target_front_matter_language_code(target_lang) + code = str(target_lang or '').strip().casefold().replace('_', '-') suffixes = [] if 'moderation' in missing: label = role_label.get('moderator') or '' if code.startswith('zh'): - suffixes.append(f'{label}的调节作用' if label else '调节作用') + suffixes.append(f'调节变量:{label}' if label else '调节作用') elif code.startswith('ru'): - suffixes.append(f'модерирующая роль переменной «{label}»' if label else 'модерирующая роль') + suffixes.append(f'модератор: {label}' if label else 'модерация') elif code.startswith('kk') or code.startswith('kz'): - suffixes.append(f'«{label}» айнымалысының модераторлық рөлі' if label else 'модераторлық рөл') + suffixes.append(f'модератор: {label}' if label else 'модераторлық рөл') else: - suffixes.append(f'the moderating role of {label}' if label else 'moderation') + suffixes.append(f'moderator: {label}' if label else 'moderation') if missing.intersection({'mediation', 'serial_mediation', 'parallel_mediation'}): label = role_label.get('mediator') or '' if code.startswith('zh'): - suffixes.append(f'{label}的中介作用' if label else '中介作用') + suffixes.append(f'中介变量:{label}' if label else '中介作用') elif code.startswith('ru'): - suffixes.append(f'медиирующая роль переменной «{label}»' if label else 'медиирующая роль') + suffixes.append(f'медиатор: {label}' if label else 'медиация') elif code.startswith('kk') or code.startswith('kz'): - suffixes.append(f'«{label}» айнымалысының медиаторлық рөлі' if label else 'медиаторлық рөл') + suffixes.append(f'медиатор: {label}' if label else 'медиаторлық рөл') else: - suffixes.append(f'the mediating role of {label}' if label else 'mediation') + suffixes.append(f'mediator: {label}' if label else 'mediation') if suffixes: - separator = ':' if code.startswith('zh') else ': ' - joiner = ';' if code.startswith('zh') else '; ' - repaired_title = title.rstrip(' .;::') + separator + joiner.join(suffixes) + repaired_title = title.rstrip(' .;:') + ' (' + '; '.join(suffixes) + ')' value = value[:title_match.start(1)] + repaired_title + value[title_match.end(1):] diagnostics.append({'type':'target_user_relation_title_rebound','missing':sorted(missing)}) return value, diagnostics @@ -3108,8 +2120,7 @@ class GenerationRunner: # R7-R2C-R5: user keywords are research-intent identities, not expendable # planning seeds. Freeze them for multilingual inheritance while still # distinguishing them from empirical construct roles. - projection = dict(meta.get('front_matter_language_projection') or {}) - intent_keywords = list(projection.get('english_keywords') or user_intent.get('source_keywords') or []) + intent_keywords = list(user_intent.get('source_keywords') or []) if not intent_keywords: intent_keywords = parse_keyword_terms(keywords, explicit_user=True) for seed in intent_keywords: @@ -4160,35 +3171,11 @@ class GenerationRunner: entitlement_class = str(snapshot.get("entitlement_class") or "").strip().upper() if tier not in {"STANDARD", "FLAGSHIP"}: raise RuntimeError("trusted_task_policy_product_tier_invalid") - - # Keep runner validation aligned with the executor's trusted-policy - # contract. Entitlement authority comes from the SHA-bound resolved - # ProductPolicy, not from a duplicated runtime enum. This preserves - # fail-closed behavior while allowing FLAGSHIP_TRIAL when (and only - # when) the trusted trial policy explicitly authorizes it. - trial = resolved.get("trial") if isinstance(resolved.get("trial"), dict) else None - billing = resolved.get("billing") if isinstance(resolved.get("billing"), dict) else None - allowed_entitlements: list[str] = [] - if trial and str(trial.get("entitlement_class") or "").strip(): - allowed_entitlements.append(str(trial.get("entitlement_class") or "").strip().upper()) - if billing and str(billing.get("entitlement_class") or "").strip(): - paid_class = str(billing.get("entitlement_class") or "").strip().upper() - if paid_class not in allowed_entitlements: - allowed_entitlements.append(paid_class) - - # Backward-compatible trusted snapshots created before the nested - # trial/billing objects were embedded still carry SHA-bound policy - # version links. Derive their same-tier classes from those links - # rather than reopening a duplicated global entitlement whitelist. - if not allowed_entitlements: - if str(resolved.get("trial_policy_version") or "").strip(): - allowed_entitlements.append(f"{tier}_TRIAL") - if str(resolved.get("billing_policy_version") or "").strip(): - allowed_entitlements.append(f"{tier}_PAID") - - if not entitlement_class or entitlement_class not in allowed_entitlements: + if entitlement_class not in {"STANDARD_TRIAL", "STANDARD_PAID", "FLAGSHIP_PAID"}: raise RuntimeError("trusted_task_policy_entitlement_invalid") - if any(not value.startswith(f"{tier}_") for value in allowed_entitlements): + if tier == "STANDARD" and entitlement_class == "FLAGSHIP_PAID": + raise RuntimeError("trusted_task_policy_entitlement_mismatch") + if tier == "FLAGSHIP" and entitlement_class != "FLAGSHIP_PAID": raise RuntimeError("trusted_task_policy_entitlement_mismatch") if str(resolved.get("product_tier") or "").strip().upper() != tier: raise RuntimeError("trusted_task_policy_product_tier_mismatch") @@ -4398,25 +3385,9 @@ class GenerationRunner: agent.global_memory["core_golden_metadata"] = extract_golden_metadata(agent, all_paper_texts) if mode == "amj": data_file = data_paths[0] if data_paths else None - # L2-R1: establish the validated English identity projection - # BEFORE a system-generated quantitative construct/statistical - # contract is frozen. The immutable source Title/Keywords remain - # semantic authority and are passed separately below. - upstream_projection = self._resolve_english_front_matter_projection( - agent, state, data_metadata, topic, keywords - ) - internal_topic_en = str( - upstream_projection.get("english_title") or topic or "" - ).strip() - internal_keywords_en = list(upstream_projection.get("english_keywords") or []) _, user_data_md, _, data_metadata = quant_engine.resolve_empirical_data( - data_file, interface_language, - topic=topic, keywords=keywords, - internal_topic=internal_topic_en, - internal_keywords=internal_keywords_en, - agent=agent, + data_file, interface_language, topic=topic, keywords=keywords, agent=agent ) - data_metadata["front_matter_language_projection"] = dict(upstream_projection) else: raw_interviews = extract_text_from_files(interview_paths[:1]) if interview_paths else "" prewrite_started = time.monotonic() @@ -4520,15 +3491,6 @@ class GenerationRunner: state.pop("effective_topic", None) state.pop("effective_keywords", None) - front_matter_projection = self._resolve_english_front_matter_projection( - agent, state, data_metadata, source_topic, source_keywords - ) - reader_topic_en = str(front_matter_projection.get("english_title") or source_topic or topic or "").strip() - reader_keywords_en = list(front_matter_projection.get("english_keywords") or []) - reader_keywords_prompt = "; ".join(reader_keywords_en) - data_metadata["front_matter_language_projection"] = dict(front_matter_projection) - state["data_metadata"] = data_metadata - hypothesis_ledger = dict(data_metadata.get("hypothesis_ledger") or {}) if mode == "amj" else {} hypothesis_contract = self._hypothesis_contract_directive(hypothesis_ledger) if hypothesis_ledger: @@ -4595,28 +3557,9 @@ class GenerationRunner: "but do not invent citations or bibliographic metadata and do not present general model knowledge as retrieved evidence." ) else: - try: - synthesis_result = self._stream_text( - agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context) - ) - except PipelineCancelledError: - raise - except Exception as exc: - synthesis_result = ( - "LITERATURE-SYNTHESIS FALLBACK: retrieved literature remains available through the citation/RAG layer, " - "but the narrative synthesis provider step was unavailable. Continue from the frozen study evidence and " - "claim-specific retrieved sources; do not invent bibliographic facts, expected findings, or causal claims." - ) - quality_diagnostics.append({ - "scope": "delivery_first_literature_synthesis_fallback", - "name": "Literature Synthesis", - "status": "deterministic_fallback", - "reason": type(exc).__name__, - }) - logging.warning( - "Delivery-first literature synthesis fallback activated | error=%s", - type(exc).__name__, - ) + synthesis_result = self._stream_text( + agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context) + ) agent.record_prewrite_stage("literature_synthesis", prewrite_started, prewrite_calls) synthesis_result = re.sub(r".*?", "", synthesis_result, flags=re.DOTALL).strip() state.update({"synthesis_result": synthesis_result, "agent_state": agent.export_state()}) @@ -4627,93 +3570,43 @@ class GenerationRunner: self._progress(14, "research_logic") prewrite_started = time.monotonic() prewrite_calls = agent.ds_calls - research_logic_error = None - try: - research_logic_response = self._stream_text( - agent.stream_generate_research_logic( - topic, keywords, synthesis_result, refs_text, - data_evidence=planning_data_md, mode=mode, - hypothesis_contract=hypothesis_contract, - ) - ) - except PipelineCancelledError: - raise - except Exception as exc: - research_logic_error = exc - research_logic_response = "" - logging.warning( - "Delivery-first research-logic provider path unavailable; using deterministic planning fallback | error=%s", - type(exc).__name__, + research_logic_response = self._stream_text( + agent.stream_generate_research_logic( + topic, keywords, synthesis_result, refs_text, + data_evidence=planning_data_md, mode=mode, + hypothesis_contract=hypothesis_contract, ) - finally: - agent.record_prewrite_stage("research_logic", prewrite_started, prewrite_calls) + ) + agent.record_prewrite_stage("research_logic", prewrite_started, prewrite_calls) research_logic_contract = re.sub( r".*?", "", research_logic_response, flags=re.DOTALL ).strip() - logic_hypothesis_rows = [] - if hypothesis_ledger and research_logic_contract: + if hypothesis_ledger: research_logic_contract = self._normalize_hypothesis_ordinal_aliases( research_logic_contract, hypothesis_ledger ) logic_hypothesis_rows = self._hypothesis_contract_violations( research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract" ) - if not research_logic_contract or logic_hypothesis_rows: - fallback_reason = ( - "provider_error" if research_logic_error is not None - else "empty_contract" if not research_logic_contract - else "hypothesis_identity_mismatch" - ) - research_logic_contract = self._deterministic_research_logic_contract( - mode, topic, keywords, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - fallback_rows = ( - self._hypothesis_contract_violations( - research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract" - ) if hypothesis_ledger else [] - ) - if fallback_rows: - # This is an internal deterministic invariant failure, not a - # model-quality rejection. Do not continue with a corrupted - # H-identity planning contract. + if logic_hypothesis_rows: raise RuntimeError( - f"delivery_first_research_logic_fallback_invalid:{fallback_rows[:3]}" + f"research_logic_hypothesis_contract_invalid:{logic_hypothesis_rows[:3]}" ) - quality_diagnostics.append({ - "scope": "delivery_first_research_logic_fallback", - "name": "Research Logic Contract", - "status": "deterministic_fallback", - "reason": fallback_reason, - "prior_violations": logic_hypothesis_rows[:3], - }) - logging.warning( - "Delivery-first Research Logic fallback activated | reason=%s prior_violations=%s", - fallback_reason, len(logic_hypothesis_rows), - ) + if not research_logic_contract: + raise RuntimeError("research_logic_contract_missing") agent.global_memory["research_logic_contract"] = research_logic_contract self._progress(14, "working_front_matter") - working_front_error = None - try: - working_front = agent.generate_working_front_matter( - reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result, - data_evidence=planning_data_md, mode=mode, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - except PipelineCancelledError: - raise - except Exception as exc: - working_front_error = exc - working_front = {} - logging.warning( - "Delivery-first Working Front Matter provider path unavailable; preparing deterministic fallback | error=%s", - type(exc).__name__, - ) + working_front = agent.generate_working_front_matter( + topic, keywords, research_logic_contract, synthesis_result, + data_evidence=planning_data_md, mode=mode, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) working_abstract = str(working_front.get("abstract") or "").strip() - if hypothesis_ledger and working_abstract: + if hypothesis_ledger: working_abstract = self._normalize_hypothesis_ordinal_aliases( working_abstract, hypothesis_ledger ) @@ -4721,22 +3614,7 @@ class GenerationRunner: mode, list(working_front.get("keywords") or []), data_metadata ) if not working_abstract or not working_keywords: - fallback = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - working_abstract = str(fallback.get("abstract") or "").strip() - working_keywords = list(fallback.get("keywords") or []) - quality_diagnostics.append({ - "scope": "delivery_first_working_front_matter_fallback", - "name": "Working Abstract", - "status": "deterministic_fallback", - "reason": "provider_error" if working_front_error is not None else "missing_component", - }) - logging.warning( - "Delivery-first Working Front Matter fallback activated | reason=%s", - "provider_error" if working_front_error is not None else "missing_component", - ) - + raise RuntimeError("working_front_matter_missing") _, front_fact_rows = self._repair_validate_study_fact_contract( mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract", @@ -4751,32 +3629,22 @@ class GenerationRunner: "Working Abstract fact validation retry | mode=%s count=%s types=%s", mode, len(front_fact_rows), sorted({str(row.get("type") or "") for row in front_fact_rows}), ) - try: - working_front = agent.generate_working_front_matter( - reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result, - data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - retry_abstract = str(working_front.get("abstract") or "").strip() - if hypothesis_ledger and retry_abstract: - retry_abstract = self._normalize_hypothesis_ordinal_aliases( - retry_abstract, hypothesis_ledger - ) - retry_keywords = self._canonical_quantitative_front_matter_keywords( - mode, list(working_front.get("keywords") or []), data_metadata - ) - if retry_abstract and retry_keywords: - working_abstract, working_keywords = retry_abstract, retry_keywords - except PipelineCancelledError: - raise - except Exception as exc: - logging.warning( - "Working Front Matter validation retry unavailable; continuing to deterministic/adaptive fallback | error=%s", - type(exc).__name__, + working_front = agent.generate_working_front_matter( + topic, keywords, research_logic_contract, synthesis_result, + data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) + working_abstract = str(working_front.get("abstract") or "").strip() + if hypothesis_ledger: + working_abstract = self._normalize_hypothesis_ordinal_aliases( + working_abstract, hypothesis_ledger ) + working_keywords = self._canonical_quantitative_front_matter_keywords( + mode, list(working_front.get("keywords") or []), data_metadata + ) _, remaining_front_rows = self._repair_validate_study_fact_contract( mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract", ) @@ -4797,30 +3665,15 @@ class GenerationRunner: "scope":"working_abstract_study_fact_integrity", "name":"Working Abstract", "status":"section_retry_resolved", "count":len(front_fact_rows), "examples":front_fact_rows[:3], }) - if not working_abstract or not working_keywords: - fallback = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - working_abstract = str(fallback.get("abstract") or "").strip() - working_keywords = list(fallback.get("keywords") or []) - quality_diagnostics.append({ - "scope": "delivery_first_working_front_matter_fallback", - "name": "Working Abstract", - "status": "deterministic_fallback_after_validation", - "reason": "empty_after_repair", - }) - logging.warning( - "Delivery-first Working Front Matter fallback activated after validation repair exhaustion." - ) - agent.global_memory["working_abstract"] = working_abstract - + if not working_abstract: + raise RuntimeError("working_front_matter_empty_after_validation") agent.global_memory["working_abstract"] = working_abstract self._progress(14, "outline_planning") plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata) # User title is reader-facing authority. Outline planning may adapt # structure but may not silently rename the study. - en_topic = str(reader_topic_en or source_topic or topic).strip() + en_topic = str(source_topic or topic).strip() structure = plan.get("structure", []) full_draft_en = ( f"# {en_topic}\n\n## Abstract\n{working_abstract}\n\n" @@ -5015,7 +3868,7 @@ class GenerationRunner: else: full_draft_en = str(state.get("full_draft_en") or "") if str(data_metadata.get("source_type") or "") == "system_generated" and mode == "amj": - full_draft_en = self._replace_title(full_draft_en, reader_topic_en) + full_draft_en = self._replace_title(full_draft_en, topic) canonical_resume_keywords = self._canonical_quantitative_front_matter_keywords( mode, [], data_metadata ) @@ -5033,7 +3886,6 @@ class GenerationRunner: research_logic_contract = str( state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or "" ) - resumed_logic_rows = [] if hypothesis_ledger and research_logic_contract: research_logic_contract = self._normalize_hypothesis_ordinal_aliases( research_logic_contract, hypothesis_ledger @@ -5041,33 +3893,11 @@ class GenerationRunner: resumed_logic_rows = self._hypothesis_contract_violations( research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract" ) - if not research_logic_contract or resumed_logic_rows: - research_logic_contract = self._deterministic_research_logic_contract( - mode, topic, keywords, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - fallback_rows = ( - self._hypothesis_contract_violations( - research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract" - ) if hypothesis_ledger else [] - ) - if fallback_rows: + if resumed_logic_rows: raise RuntimeError( - f"delivery_first_research_logic_fallback_invalid_on_resume:{fallback_rows[:3]}" + f"research_logic_hypothesis_contract_invalid_on_resume:{resumed_logic_rows[:3]}" ) - quality_diagnostics.append({ - "scope": "delivery_first_research_logic_fallback", - "name": "Research Logic Contract", - "status": "deterministic_resume_fallback", - "reason": "missing_contract" if not str(state.get("research_logic_contract") or "").strip() else "hypothesis_identity_mismatch", - "prior_violations": resumed_logic_rows[:3], - }) - state["research_logic_contract"] = research_logic_contract - state["quality_diagnostics"] = quality_diagnostics - logging.warning( - "Delivery-first Research Logic resume fallback activated | prior_violations=%s", - len(resumed_logic_rows), - ) - agent.global_memory["research_logic_contract"] = research_logic_contract + agent.global_memory["research_logic_contract"] = research_logic_contract citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {} citation_minimums = dict(state.get("citation_minimums") or {}) citation_reserve_targets = dict(state.get("citation_reserve_targets") or {}) @@ -5132,41 +3962,22 @@ class GenerationRunner: )) if resumed_front_rows: feedback = self._section_retry_feedback(resumed_front_rows) - try: - working_front = agent.generate_working_front_matter( - reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result, - data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - candidate_abstract = str(working_front.get("abstract") or "").strip() - if hypothesis_ledger and candidate_abstract: - candidate_abstract = self._normalize_hypothesis_ordinal_aliases( - candidate_abstract, hypothesis_ledger - ) - candidate_keywords = self._canonical_quantitative_front_matter_keywords( - mode, list(working_front.get("keywords") or []), data_metadata - ) - except PipelineCancelledError: - raise - except Exception as exc: - logging.warning( - "Resumed Working Front Matter repair unavailable; using deterministic fallback | error=%s", - type(exc).__name__, - ) - fallback = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - candidate_abstract = str(fallback.get("abstract") or "").strip() - candidate_keywords = list(fallback.get("keywords") or []) - if not candidate_abstract or not candidate_keywords: - fallback = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger + working_front = agent.generate_working_front_matter( + topic, keywords, research_logic_contract, synthesis_result, + data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) + candidate_abstract = str(working_front.get("abstract") or "").strip() + if hypothesis_ledger: + candidate_abstract = self._normalize_hypothesis_ordinal_aliases( + candidate_abstract, hypothesis_ledger ) - candidate_abstract = str(fallback.get("abstract") or "").strip() - candidate_keywords = list(fallback.get("keywords") or []) + candidate_keywords = self._canonical_quantitative_front_matter_keywords( + mode, list(working_front.get("keywords") or []), data_metadata + ) _, remaining = self._repair_validate_study_fact_contract( mode, agent, quant_engine, candidate_abstract, data_metadata, section_name="Working Abstract", ) @@ -5183,12 +3994,6 @@ class GenerationRunner: ) if residual_front_rows: logging.warning("Adaptive resumed Working Abstract accepted with residual diagnostics | count=%s", len(residual_front_rows)) - if not candidate_abstract: - fallback = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - candidate_abstract = str(fallback.get("abstract") or "").strip() - candidate_keywords = list(fallback.get("keywords") or candidate_keywords) working_abstract = candidate_abstract working_keywords = candidate_keywords or working_keywords state["working_abstract"] = working_abstract @@ -5386,33 +4191,16 @@ class GenerationRunner: # an outline but no Working Abstract yet. Generate it once, checkpoint it, # and reuse it thereafter; never regenerate an unchanged anchor on resume. if not working_abstract and not state.get("english_finalized"): - try: - working_front = agent.generate_working_front_matter( - reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result, - data_evidence=planning_data_md, mode=mode, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - except PipelineCancelledError: - raise - except Exception as exc: - logging.warning( - "Migrated Working Front Matter provider path unavailable; using deterministic fallback | error=%s", - type(exc).__name__, - ) - working_front = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - quality_diagnostics.append({ - "scope": "delivery_first_working_front_matter_fallback", - "name": "Working Abstract", - "status": "deterministic_resume_migration_fallback", - "reason": "provider_error", - }) + working_front = agent.generate_working_front_matter( + topic, keywords, research_logic_contract, synthesis_result, + data_evidence=planning_data_md, mode=mode, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) working_abstract = str(working_front.get("abstract") or "").strip() - if hypothesis_ledger and working_abstract: + if hypothesis_ledger: working_abstract = self._normalize_hypothesis_ordinal_aliases( working_abstract, hypothesis_ledger ) @@ -5420,17 +4208,7 @@ class GenerationRunner: mode, list(working_front.get("keywords") or []), data_metadata ) if not working_abstract or not working_keywords: - working_front = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - working_abstract = str(working_front.get("abstract") or "").strip() - working_keywords = list(working_front.get("keywords") or []) - quality_diagnostics.append({ - "scope": "delivery_first_working_front_matter_fallback", - "name": "Working Abstract", - "status": "deterministic_resume_migration_fallback", - "reason": "missing_component", - }) + raise RuntimeError("working_front_matter_missing") _, front_fact_rows = self._repair_validate_study_fact_contract( mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract", ) @@ -5440,32 +4218,22 @@ class GenerationRunner: )) if front_fact_rows: feedback = self._section_retry_feedback(front_fact_rows) - try: - working_front = agent.generate_working_front_matter( - reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result, - data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - retry_abstract = str(working_front.get("abstract") or "").strip() - if hypothesis_ledger and retry_abstract: - retry_abstract = self._normalize_hypothesis_ordinal_aliases( - retry_abstract, hypothesis_ledger - ) - retry_keywords = self._canonical_quantitative_front_matter_keywords( - mode, list(working_front.get("keywords") or []), data_metadata - ) - if retry_abstract and retry_keywords: - working_abstract, working_keywords = retry_abstract, retry_keywords - except PipelineCancelledError: - raise - except Exception as exc: - logging.warning( - "Migrated Working Front Matter validation retry unavailable; preserving deterministic-safe candidate | error=%s", - type(exc).__name__, + working_front = agent.generate_working_front_matter( + topic, keywords, research_logic_contract, synthesis_result, + data_evidence=planning_data_md, mode=mode, validation_feedback=feedback, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) + working_abstract = str(working_front.get("abstract") or "").strip() + if hypothesis_ledger: + working_abstract = self._normalize_hypothesis_ordinal_aliases( + working_abstract, hypothesis_ledger ) + working_keywords = self._canonical_quantitative_front_matter_keywords( + mode, list(working_front.get("keywords") or []), data_metadata + ) _, remaining = self._repair_validate_study_fact_contract( mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract", ) @@ -5486,13 +4254,8 @@ class GenerationRunner: "scope":"working_abstract_study_fact_integrity", "name":"Working Abstract", "status":"section_retry_resolved", "count":len(front_fact_rows), "examples":front_fact_rows[:3], }) - if not working_abstract or not working_keywords: - working_front = self._deterministic_working_front_matter( - mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - working_abstract = str(working_front.get("abstract") or "").strip() - working_keywords = list(working_front.get("keywords") or []) - logging.warning("Delivery-first migrated Working Front Matter fallback activated after repair exhaustion.") + if not working_abstract: + raise RuntimeError("working_front_matter_empty_after_validation") agent.global_memory["working_abstract"] = working_abstract full_draft_en = replace_abstract(full_draft_en, working_abstract) full_draft_en = self._replace_keywords(full_draft_en, working_keywords) @@ -5501,7 +4264,6 @@ class GenerationRunner: "working_keywords": working_keywords, "front_matter_phase": "working", "full_draft_en": full_draft_en, - "quality_diagnostics": quality_diagnostics, "agent_state": agent.export_state(), }) self._publish_preview(full_draft_en, "working_front_matter", force=True) @@ -5600,11 +4362,8 @@ class GenerationRunner: ) sec_title = sec["section"] subsection = sec.get("subsection") - is_quarantined = any( - word in sec_title.lower() - for word in ["abstract", "keyword", "conclusion", "references", "appendix"] - ) section_id = str(sec.get("section_id") or "") + is_quarantined = self._section_forbids_external_citations(sec_title, section_id) section_logical_node = self._section_logical_node( mode, section_id, sec.get("subsection_id") ) @@ -5630,7 +4389,7 @@ class GenerationRunner: elif section_id == "conclusion": mode_directive = str(APP_CONFIG.get("conclusion_anchor_directive") or "") - if is_quarantined or section_id in {"results", "findings"}: + if is_quarantined: citation_rule = ( "NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. " "Report only documented empirical/qualitative evidence and the interpretation permitted by the section role." @@ -5895,19 +4654,17 @@ Output only final academic prose. Do not report the word count.""" candidate_user_prompt = user_prompt if retry_excluded_tags: retry_claim_plan = agent.format_claim_evidence_plan(planned_items) - candidate_user_prompt = _regex_sub_literal( + candidate_user_prompt = re.sub( r".*?", f"\n{retry_claim_plan}\n", - candidate_user_prompt, - flags=re.S, + candidate_user_prompt, flags=re.S, ) retry_capability_context = self._section_citation_capability_context(agent, planned_items) - candidate_user_prompt = _regex_sub_literal( + candidate_user_prompt = re.sub( r"\n.*?\n", (f"\n\n{retry_capability_context}\n\n" if retry_capability_context else "\n"), - candidate_user_prompt, - flags=re.S, + candidate_user_prompt, flags=re.S, ) if attempt_feedback: excluded_text = ", ".join(sorted(retry_excluded_tags, key=agent._citation_tag_number)) or "None" @@ -6097,6 +4854,29 @@ The previous attempt for this same manuscript unit was rejected by validation. R if adaptive_advisory: self._record_section_advisories(quality_notes, adaptive_advisory, section_name=display_name) if adaptive_blocking: + # P3-A4: citation identity/scope authority is a hard + # pre-seal invariant. Adaptive completion may retain + # reader-facing advisories for unrelated quality + # defects, but it may never seal a Section that still + # contains an unauthorized/ambiguous citation identity + # or a citation outside the frozen unit whitelist. + persistent_citation_authority = self._persistent_citation_authority_rows( + adaptive_blocking + ) + if persistent_citation_authority: + self._checkpoint_section_retry_exhausted( + state, section_id=section_id, section_name=display_name, + violations=persistent_citation_authority, pct=pct, attempts=section_attempt, + ) + raise RuntimeError( + "citation_authority_repair_exhausted:" + + str(display_name) + + ":" + + ",".join(sorted({ + str((row or {}).get("type") or "citation_authority") + for row in persistent_citation_authority if isinstance(row, dict) + })) + ) self._record_adaptive_completion_residual( quality_notes, adaptive_blocking, section_name=display_name, stage="phase1_semantic_repair" ) @@ -6276,7 +5056,26 @@ The previous attempt for this same manuscript unit was rejected by validation. R study_evidence=writer_scoped_data_md, evidence_contract=evidence_packet, hypothesis_ledger=hypothesis_ledger, task=sec, quote_invocation_plan=quote_invocation_plan, quality_notes=quality_notes, + allowed_citation_tags=allowed_section_citation_tags, + quarantine=is_quarantined, + ) + persistent_citation_authority = self._persistent_citation_authority_rows( + residual_rows ) + if persistent_citation_authority: + self._checkpoint_section_retry_exhausted( + state, section_id=section_id, section_name=display_name, + violations=persistent_citation_authority, pct=pct, attempts=section_attempt, + ) + raise RuntimeError( + "citation_authority_repair_exhausted:" + + str(display_name) + + ":" + + ",".join(sorted({ + str((row or {}).get("type") or "citation_authority") + for row in persistent_citation_authority if isinstance(row, dict) + })) + ) logging.warning( "Adaptive completion accepted section | unit=%s residual=%s", display_name, len(residual_rows), @@ -6491,41 +5290,6 @@ The previous attempt for this same manuscript unit was rejected by validation. R full_draft_en, hypothesis_ledger, section_name="Full Manuscript", require_all=True ) if hypothesis_rows: - reconciled, reconciliation_notes = self._deterministic_hypothesis_reconcile( - full_draft_en, hypothesis_ledger - ) - reconciled_rows = self._hypothesis_contract_violations( - reconciled, hypothesis_ledger, section_name="Full Manuscript", require_all=True - ) - if reconciliation_notes: - self._record_adaptive_completion_residual( - quality_notes, reconciliation_notes, section_name="Full Manuscript", - stage="delivery_first_hypothesis_reconciliation" - ) - if reconciled_rows: - self._raise_if_fatal_completion_residual( - reconciled_rows, section_name="Full Manuscript", mode=mode, - error_code="final_hypothesis_contract_unresolved" - ) - else: - full_draft_en = reconciled - changed_sections = self._refresh_section_integrity_seals_after_delivery_repair( - full_draft_en, section_order_for_fact, section_integrity_seals - ) - if changed_sections: - requeued = self._requeue_resealed_translation_sections( - changed_sections=changed_sections, mode=mode, quant_engine=quant_engine, - data_metadata=data_metadata, translation_agent=translation_agent, - translation_sections=translation_sections, english_commit_text=full_draft_en, - ) - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "delivery_first_section_resealed", - "count": len(changed_sections), - "requeued_translations": requeued, - "reason": "Deterministic H-identity repair changed sealed English bytes; section seals and target translation source authority were refreshed before finalization.", - }], section_name="Full Manuscript", stage="delivery_first_hypothesis_reseal" - ) self._record_adaptive_completion_residual( quality_notes, hypothesis_rows, section_name="Full Manuscript", stage="final_hypothesis_review" ) @@ -6638,32 +5402,13 @@ The previous attempt for this same manuscript unit was rejected by validation. R # from the completed manuscript/evidence, then replace the provisional # front matter deterministically. No body rebalance may occur afterward. self._progress(92, "final_front_matter") - try: - final_front = agent.generate_final_front_matter( - reader_topic_en, reader_keywords_prompt, body_only, data_evidence=user_data_md, mode=mode, - hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - except Exception as exc: - logging.warning( - "Delivery-first Final Front Matter fallback | mode=%s error=%s", mode, type(exc).__name__ - ) - final_front = { - "abstract": self._deterministic_final_abstract( - mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger - ), - "keywords": self._canonical_quantitative_front_matter_keywords( - mode, list(reader_keywords_en), data_metadata - ) or self._front_matter_keyword_fallback_terms(data_metadata), - } - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "final_front_matter_provider_fallback", - "reason": f"Final Front Matter model/validation failed ({type(exc).__name__}); deterministic frozen-authority fallback was used." - }], section_name="Final Abstract", stage="delivery_first_front_matter_fallback" - ) + final_front = agent.generate_final_front_matter( + topic, keywords, body_only, data_evidence=user_data_md, mode=mode, + hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) final_abstract = str(final_front.get("abstract") or "").strip() if hypothesis_ledger: final_abstract = self._normalize_hypothesis_ordinal_aliases( @@ -6673,18 +5418,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R mode, list(final_front.get("keywords") or []), data_metadata ) if not final_abstract or not final_keywords: - final_abstract = final_abstract or self._deterministic_final_abstract( - mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords( - mode, list(reader_keywords_en), data_metadata - ) or self._front_matter_keyword_fallback_terms(data_metadata) - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "final_front_matter_missing_fallback", - "reason": "Missing Final Front Matter component was reconstructed deterministically from frozen title/keyword/finding authority." - }], section_name="Final Abstract", stage="delivery_first_front_matter_fallback" - ) + raise RuntimeError("final_front_matter_missing") final_front_fact_rows = self._study_fact_violations( mode, agent, quant_engine, final_abstract, data_metadata, section_name="Final Abstract", @@ -6693,9 +5427,6 @@ The previous attempt for this same manuscript unit was rejected by validation. R final_front_fact_rows.extend(self._hypothesis_contract_violations( final_abstract, hypothesis_ledger, section_name="Final Abstract" )) - final_front_fact_rows.extend(self._final_abstract_finding_violations( - mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger - )) if final_front_fact_rows: feedback = self._section_retry_feedback(final_front_fact_rows) logging.warning( @@ -6703,26 +5434,13 @@ The previous attempt for this same manuscript unit was rejected by validation. R mode, len(final_front_fact_rows), sorted({str(row.get("type") or "") for row in final_front_fact_rows if isinstance(row, dict)}), ) - try: - final_front = agent.generate_final_front_matter( - reader_topic_en, reader_keywords_prompt, body_only, data_evidence=user_data_md, mode=mode, - validation_feedback=feedback, hypothesis_contract=hypothesis_contract, - authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), - keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), - fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), - ) - except Exception as exc: - logging.warning( - "Delivery-first Final Front Matter retry fallback | mode=%s error=%s", mode, type(exc).__name__ - ) - final_front = { - "abstract": self._deterministic_final_abstract( - mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger - ), - "keywords": final_keywords or self._canonical_quantitative_front_matter_keywords( - mode, list(reader_keywords_en), data_metadata - ) or self._front_matter_keyword_fallback_terms(data_metadata), - } + final_front = agent.generate_final_front_matter( + topic, keywords, body_only, data_evidence=user_data_md, mode=mode, + validation_feedback=feedback, hypothesis_contract=hypothesis_contract, + authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata), + keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata), + fallback_keywords=self._front_matter_keyword_fallback_terms(data_metadata), + ) final_abstract = str(final_front.get("abstract") or "").strip() if hypothesis_ledger: final_abstract = self._normalize_hypothesis_ordinal_aliases( @@ -6732,18 +5450,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R mode, list(final_front.get("keywords") or []), data_metadata ) if not final_abstract or not final_keywords: - final_abstract = final_abstract or self._deterministic_final_abstract( - mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords( - mode, list(reader_keywords_en), data_metadata - ) or self._front_matter_keyword_fallback_terms(data_metadata) - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "final_front_matter_retry_exhausted_fallback", - "reason": "Final Front Matter remained incomplete after retry; deterministic frozen-authority fallback was used." - }], section_name="Final Abstract", stage="delivery_first_front_matter_fallback" - ) + raise RuntimeError("final_front_matter_missing_after_retry") remaining_final_fact_rows = self._study_fact_violations( mode, agent, quant_engine, final_abstract, data_metadata, section_name="Final Abstract", @@ -6752,32 +5459,6 @@ The previous attempt for this same manuscript unit was rejected by validation. R remaining_final_fact_rows.extend(self._hypothesis_contract_violations( final_abstract, hypothesis_ledger, section_name="Final Abstract" )) - remaining_final_fact_rows.extend(self._final_abstract_finding_violations( - mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger - )) - if remaining_final_fact_rows: - resilient_abstract, resilience_notes = self._final_abstract_resilience_repair( - mode, final_abstract, remaining_final_fact_rows, data_metadata, - hypothesis_ledger=hypothesis_ledger - ) - if resilient_abstract != final_abstract: - final_abstract = resilient_abstract - remaining_final_fact_rows = self._study_fact_violations( - mode, agent, quant_engine, final_abstract, data_metadata, - section_name="Final Abstract", - ) - if hypothesis_ledger: - remaining_final_fact_rows.extend(self._hypothesis_contract_violations( - final_abstract, hypothesis_ledger, section_name="Final Abstract" - )) - remaining_final_fact_rows.extend(self._final_abstract_finding_violations( - mode, final_abstract, data_metadata, hypothesis_ledger=hypothesis_ledger - )) - if resilience_notes: - self._record_adaptive_completion_residual( - quality_notes, resilience_notes, section_name="Final Abstract", - stage="final_abstract_resilience_recovery" - ) if remaining_final_fact_rows: final_abstract, residual_final_rows = self._adaptive_repair_front_matter( agent=agent, quant_engine=quant_engine, abstract_text=final_abstract, @@ -6786,42 +5467,19 @@ The previous attempt for this same manuscript unit was rejected by validation. R quality_notes=quality_notes, label="Final Abstract", ) if residual_final_rows: - deterministic_abstract = self._deterministic_final_abstract( - mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger - ) - hard_rows = self._study_fact_violations( - mode, agent, quant_engine, deterministic_abstract, data_metadata, section_name="Final Abstract" - ) - if hypothesis_ledger: - hard_rows.extend(self._hypothesis_contract_violations( - deterministic_abstract, hypothesis_ledger, section_name="Final Abstract" - )) - if hard_rows: - self._raise_if_fatal_completion_residual( - hard_rows, section_name="Final Abstract", mode=mode, - error_code="final_abstract_integrity_unresolved" - ) - final_abstract = deterministic_abstract - self._record_adaptive_completion_residual( - quality_notes, residual_final_rows, section_name="Final Abstract", - stage="delivery_first_final_abstract_fallback" - ) - logging.warning( - "Delivery-first Final Abstract deterministic fallback accepted | residual_diagnostics=%s", - len(residual_final_rows) - ) + logging.warning("Adaptive Final Abstract accepted with residual diagnostics | count=%s", len(residual_final_rows)) quality_diagnostics.append({ "scope":"final_abstract_study_fact_integrity", "name":"Final Abstract", "status":"front_matter_retry_resolved", "count":len(final_front_fact_rows), "examples":final_front_fact_rows[:3], }) state.pop("final_abstract_retry_exhausted", None) - final_title = str(reader_topic_en or source_topic or topic or "").strip() + final_title = str(source_topic or topic or "").strip() if mode == "amj" and str(data_metadata.get("source_type") or "") == "system_generated": semantic_contract = dict(data_metadata.get("front_matter_contract") or {}) reconciled_front = agent.reconcile_quantitative_reader_front_matter( topic=source_topic, keywords=source_keywords, contract=semantic_contract, - manuscript=body_only, reader_title=reader_topic_en, reader_keywords=reader_keywords_en, + manuscript=body_only, ) candidate_title = re.sub(r"\s+", " ", str(reconciled_front.get("title") or "")).strip() candidate_keywords = self._canonical_quantitative_front_matter_keywords( @@ -6840,57 +5498,11 @@ The previous attempt for this same manuscript unit was rejected by validation. R full_draft_en = replace_abstract(full_draft_en, final_abstract) full_draft_en = self._replace_title(full_draft_en, final_title) full_draft_en = self._replace_keywords(full_draft_en, final_keywords) - agent.validate_reader_facing_english_projection( - {"title": final_title, "keywords": final_keywords}, - expected_keyword_count=(len(reader_keywords_en) if reader_keywords_en else None), - ) - final_abstract, language_rebuilt = self._delivery_first_final_abstract_language_guard( - mode=mode, agent=agent, quant_engine=quant_engine, final_abstract=final_abstract, - reader_topic_en=reader_topic_en, final_title=final_title, data_metadata=data_metadata, - hypothesis_ledger=hypothesis_ledger, quality_notes=quality_notes, - ) - if language_rebuilt: - full_draft_en = replace_abstract(full_draft_en, final_abstract) if mode == "amj" and hypothesis_ledger: post_front_hypothesis_rows = self._hypothesis_contract_violations( full_draft_en, hypothesis_ledger, section_name="Full Manuscript", require_all=True ) if post_front_hypothesis_rows: - reconciled, reconciliation_notes = self._deterministic_hypothesis_reconcile( - full_draft_en, hypothesis_ledger - ) - remaining_post_rows = self._hypothesis_contract_violations( - reconciled, hypothesis_ledger, section_name="Full Manuscript", require_all=True - ) - if reconciliation_notes: - self._record_adaptive_completion_residual( - quality_notes, reconciliation_notes, section_name="Final Abstract", - stage="delivery_first_post_front_hypothesis_reconciliation" - ) - if remaining_post_rows: - self._raise_if_fatal_completion_residual( - remaining_post_rows, section_name="Full Manuscript", mode=mode, - error_code="post_front_matter_hypothesis_contract_unresolved" - ) - else: - full_draft_en = reconciled - changed_sections = self._refresh_section_integrity_seals_after_delivery_repair( - full_draft_en, section_order_for_fact, section_integrity_seals - ) - if changed_sections: - requeued = self._requeue_resealed_translation_sections( - changed_sections=changed_sections, mode=mode, quant_engine=quant_engine, - data_metadata=data_metadata, translation_agent=translation_agent, - translation_sections=translation_sections, english_commit_text=full_draft_en, - ) - self._record_adaptive_completion_residual( - quality_notes, [{ - "type": "delivery_first_section_resealed", - "count": len(changed_sections), - "requeued_translations": requeued, - "reason": "Post-front-matter H-identity repair changed sealed English bytes; section seals and target translation source authority were refreshed before checkpointing.", - }], section_name="Full Manuscript", stage="delivery_first_post_front_reseal" - ) self._record_adaptive_completion_residual( quality_notes, post_front_hypothesis_rows, section_name="Final Abstract", stage="post_front_matter_hypothesis_review" @@ -7266,57 +5878,6 @@ The previous attempt for this same manuscript unit was rejected by validation. R }) return result - def build_recovery_artifact(self, error: Exception) -> dict[str, str]: - """Best-effort failed/refunded download when English authority is already frozen. - - This method never changes task success semantics. The Executor may expose - the returned ZIP only on a FAILED task after quota release. - """ - if "cancel" in str(error or "").casefold(): - return {} - latest_path = self.task_dir / "checkpoints" / "latest.json" - try: - payload = json.loads(latest_path.read_text(encoding="utf-8")) - except Exception: - return {} - if not isinstance(payload, dict) or payload.get("request_hash") != self.request_hash: - return {} - if not bool(payload.get("english_finalized")): - return {} - english = str(payload.get("full_draft_en") or "").strip() - if len(english) < 1500: - return {} - translated = str(payload.get("full_draft_translated") or "").strip() - target_lang = str(self.request.get("target_lang") or "").strip() - output_suffix = str(self.request.get("output_suffix") or "Target").strip() - target_file_language = ( - "Chinese" if "chinese" in target_lang.casefold() or "中文" in target_lang - else "Russian" if "russian" in target_lang.casefold() or "рус" in target_lang.casefold() - else "Kazakh" if "kazakh" in target_lang.casefold() or "қазақ" in target_lang.casefold() - else re.sub(r"[^A-Za-z]+", "", output_suffix) or "Target" - ) - try: - package = build_recovery_package( - english_markdown=english, - translated_markdown=translated, - english_filename="Paper_English.docx", - translated_filename=f"Paper_{target_file_language}_Partial.docx", - error_detail=str(error or "")[:1000], - ) - except Exception as recovery_exc: - logging.warning( - "Delivery-first recovery package unavailable | error=%s original=%s", - type(recovery_exc).__name__, type(error).__name__, - ) - return {} - bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M") - output_filename = f"SmartPaper_Recovery_{bj_time}.zip" - pending = self.output_dir / (".pending_" + output_filename) - pending.write_bytes(package.zip_bytes) - final = self.output_dir / output_filename - os.replace(pending, final) - return {"output_filename": output_filename, "output_path": str(final)} - def run(self) -> dict[str, Any]: try: return self._run_impl()