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()