Spaces:
Running
Running
S0-R1-C5.1 Hypothesis Authority Semantic JIT Citation Split
#79
by GuXiaopeng - opened
- core/generation_runner.py +189 -40
- core/pipeline_agent.py +166 -40
core/generation_runner.py
CHANGED
|
@@ -1047,6 +1047,36 @@ class GenerationRunner:
|
|
| 1047 |
"or change the construct pairing to fit observed results. The contract is result-independent."
|
| 1048 |
)
|
| 1049 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1050 |
@classmethod
|
| 1051 |
def _hypothesis_contract_violations(
|
| 1052 |
cls, text: str, ledger: dict, *, section_name: str = "", require_all: bool = False
|
|
@@ -1081,45 +1111,43 @@ class GenerationRunner:
|
|
| 1081 |
"reason": "Ordinal hypothesis aliases can silently reorder the canonical H1-Hn identity.",
|
| 1082 |
"repair_instruction": "Refer to hypotheses only by their frozen H1-Hn identifiers.",
|
| 1083 |
})
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1090 |
}
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
lower_sentence = re.sub(r"\s+", " ", sentence).casefold()
|
| 1106 |
-
mentioned_constructs = {
|
| 1107 |
-
term for term in all_constructs if term.casefold() in lower_sentence
|
| 1108 |
-
}
|
| 1109 |
-
# Validate pairing only when the sentence actually names one or
|
| 1110 |
-
# more constructs. A bare cross-reference ("H1 is summarized")
|
| 1111 |
-
# remains legal and cannot silently redefine the hypothesis.
|
| 1112 |
-
if mentioned_constructs and predictor and outcome and (
|
| 1113 |
-
predictor.casefold() not in lower_sentence or outcome.casefold() not in lower_sentence
|
| 1114 |
-
):
|
| 1115 |
-
rows.append({
|
| 1116 |
-
"type": "hypothesis_contract_pairing_mismatch",
|
| 1117 |
-
"id": hid,
|
| 1118 |
-
"sentence": re.sub(r"\s+", " ", sentence).strip()[:420],
|
| 1119 |
-
"expected": f"{predictor} -> {outcome}",
|
| 1120 |
-
"reason": "A canonical hypothesis label was attached to a different construct pairing.",
|
| 1121 |
-
"repair_instruction": f"Define {hid} only for {predictor} and {outcome}, using association language.",
|
| 1122 |
-
})
|
| 1123 |
if require_all:
|
| 1124 |
missing = sorted(set(expected) - observed)
|
| 1125 |
if missing:
|
|
@@ -2448,6 +2476,15 @@ class GenerationRunner:
|
|
| 2448 |
)
|
| 2449 |
state["data_metadata"] = data_metadata
|
| 2450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2451 |
self._qualitative_quote_sources = (
|
| 2452 |
self._qualitative_quote_source_map(data_metadata) if mode == "asq" else {}
|
| 2453 |
)
|
|
@@ -2522,12 +2559,24 @@ class GenerationRunner:
|
|
| 2522 |
agent.stream_generate_research_logic(
|
| 2523 |
topic, keywords, synthesis_result, refs_text,
|
| 2524 |
data_evidence=planning_data_md, mode=mode,
|
|
|
|
| 2525 |
)
|
| 2526 |
)
|
| 2527 |
agent.record_prewrite_stage("research_logic", prewrite_started, prewrite_calls)
|
| 2528 |
research_logic_contract = re.sub(
|
| 2529 |
r"<think>.*?</think>", "", research_logic_response, flags=re.DOTALL
|
| 2530 |
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2531 |
if not research_logic_contract:
|
| 2532 |
raise RuntimeError("research_logic_contract_missing")
|
| 2533 |
agent.global_memory["research_logic_contract"] = research_logic_contract
|
|
@@ -2536,8 +2585,13 @@ class GenerationRunner:
|
|
| 2536 |
working_front = agent.generate_working_front_matter(
|
| 2537 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2538 |
data_evidence=planning_data_md, mode=mode,
|
|
|
|
| 2539 |
)
|
| 2540 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2541 |
working_keywords = list(working_front.get("keywords") or [])
|
| 2542 |
if not working_abstract or not working_keywords:
|
| 2543 |
raise RuntimeError("working_front_matter_missing")
|
|
@@ -2545,6 +2599,10 @@ class GenerationRunner:
|
|
| 2545 |
mode, agent, quant_engine, working_abstract, data_metadata,
|
| 2546 |
section_name="Working Abstract",
|
| 2547 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2548 |
if front_fact_rows:
|
| 2549 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 2550 |
logging.warning(
|
|
@@ -2554,12 +2612,21 @@ class GenerationRunner:
|
|
| 2554 |
working_front = agent.generate_working_front_matter(
|
| 2555 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2556 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
|
|
|
| 2557 |
)
|
| 2558 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2559 |
working_keywords = list(working_front.get("keywords") or [])
|
| 2560 |
_, remaining_front_rows = self._repair_validate_study_fact_contract(
|
| 2561 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 2562 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2563 |
if remaining_front_rows:
|
| 2564 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining_front_rows[:3]}")
|
| 2565 |
quality_diagnostics.append({
|
|
@@ -2757,6 +2824,18 @@ class GenerationRunner:
|
|
| 2757 |
research_logic_contract = str(
|
| 2758 |
state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
|
| 2759 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2760 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 2761 |
citation_minimums = dict(state.get("citation_minimums") or {})
|
| 2762 |
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
|
|
@@ -2798,20 +2877,37 @@ class GenerationRunner:
|
|
| 2798 |
working_abstract = str(state.get("working_abstract") or "").strip()
|
| 2799 |
working_keywords = list(state.get("working_keywords") or [])
|
| 2800 |
if working_abstract and not state.get("english_finalized"):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2801 |
_, resumed_front_rows = self._repair_validate_study_fact_contract(
|
| 2802 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 2803 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2804 |
if resumed_front_rows:
|
| 2805 |
feedback = self._section_retry_feedback(resumed_front_rows)
|
| 2806 |
working_front = agent.generate_working_front_matter(
|
| 2807 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2808 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
|
|
|
| 2809 |
)
|
| 2810 |
candidate_abstract = str(working_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2811 |
candidate_keywords = list(working_front.get("keywords") or [])
|
| 2812 |
_, remaining = self._repair_validate_study_fact_contract(
|
| 2813 |
mode, agent, quant_engine, candidate_abstract, data_metadata, section_name="Working Abstract",
|
| 2814 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2815 |
if remaining:
|
| 2816 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining[:3]}")
|
| 2817 |
working_abstract = candidate_abstract
|
|
@@ -3010,25 +3106,43 @@ class GenerationRunner:
|
|
| 3010 |
working_front = agent.generate_working_front_matter(
|
| 3011 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 3012 |
data_evidence=planning_data_md, mode=mode,
|
|
|
|
| 3013 |
)
|
| 3014 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3015 |
working_keywords = list(working_front.get("keywords") or [])
|
| 3016 |
if not working_abstract or not working_keywords:
|
| 3017 |
raise RuntimeError("working_front_matter_missing")
|
| 3018 |
_, front_fact_rows = self._repair_validate_study_fact_contract(
|
| 3019 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 3020 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3021 |
if front_fact_rows:
|
| 3022 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 3023 |
working_front = agent.generate_working_front_matter(
|
| 3024 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 3025 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
|
|
|
| 3026 |
)
|
| 3027 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3028 |
working_keywords = list(working_front.get("keywords") or [])
|
| 3029 |
_, remaining = self._repair_validate_study_fact_contract(
|
| 3030 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 3031 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3032 |
if remaining:
|
| 3033 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining[:3]}")
|
| 3034 |
quality_diagnostics.append({
|
|
@@ -3414,6 +3528,16 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3414 |
en_raw = re.sub(r"(?m)^(#{2,3})\s+[\d\.\s一二三四五六七八九十、]+\s*(.*)", r"\1 \2", en_raw)
|
| 3415 |
en_raw = re.sub(r"(?m)^#{4,}\s+(.*)", r"**\1**", en_raw)
|
| 3416 |
en_raw = re.sub(r"(?m)^#\s+(.+)$", r"## \1", en_raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3417 |
if mode == "asq":
|
| 3418 |
# Model prose has no authority over quote presence/count.
|
| 3419 |
en_raw = self._strip_model_controlled_qualitative_quotes(en_raw, data_metadata)
|
|
@@ -3919,9 +4043,14 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3919 |
# front matter deterministically. No body rebalance may occur afterward.
|
| 3920 |
self._progress(92, "final_front_matter")
|
| 3921 |
final_front = agent.generate_final_front_matter(
|
| 3922 |
-
topic, keywords, body_only, data_evidence=user_data_md, mode=mode
|
|
|
|
| 3923 |
)
|
| 3924 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3925 |
final_keywords = list(final_front.get("keywords") or [])
|
| 3926 |
if not final_abstract or not final_keywords:
|
| 3927 |
raise RuntimeError("final_front_matter_missing")
|
|
@@ -3929,6 +4058,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3929 |
mode, agent, quant_engine, final_abstract, data_metadata,
|
| 3930 |
section_name="Final Abstract",
|
| 3931 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3932 |
if final_front_fact_rows:
|
| 3933 |
feedback = self._section_retry_feedback(final_front_fact_rows)
|
| 3934 |
logging.warning(
|
|
@@ -3938,9 +4071,13 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3938 |
)
|
| 3939 |
final_front = agent.generate_final_front_matter(
|
| 3940 |
topic, keywords, body_only, data_evidence=user_data_md, mode=mode,
|
| 3941 |
-
validation_feedback=feedback,
|
| 3942 |
)
|
| 3943 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3944 |
final_keywords = list(final_front.get("keywords") or [])
|
| 3945 |
if not final_abstract or not final_keywords:
|
| 3946 |
raise RuntimeError("final_front_matter_missing_after_retry")
|
|
@@ -3948,6 +4085,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3948 |
mode, agent, quant_engine, final_abstract, data_metadata,
|
| 3949 |
section_name="Final Abstract",
|
| 3950 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3951 |
if remaining_final_fact_rows:
|
| 3952 |
state["final_abstract_retry_exhausted"] = {
|
| 3953 |
"attempts": 2,
|
|
@@ -3973,6 +4114,14 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3973 |
state.pop("final_abstract_retry_exhausted", None)
|
| 3974 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 3975 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3976 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 3977 |
full_draft_en, minimum_references=final_reference_count
|
| 3978 |
)
|
|
|
|
| 1047 |
"or change the construct pairing to fit observed results. The contract is result-independent."
|
| 1048 |
)
|
| 1049 |
|
| 1050 |
+
@staticmethod
|
| 1051 |
+
def _hypothesis_memory_text(ledger: dict) -> str:
|
| 1052 |
+
"""Render the sole AMJ H1-Hn memory from the frozen ledger."""
|
| 1053 |
+
lines = []
|
| 1054 |
+
for item in (dict(ledger or {}).get("items") or []):
|
| 1055 |
+
if not isinstance(item, dict):
|
| 1056 |
+
continue
|
| 1057 |
+
hid = str(item.get("hypothesis_id") or "").strip().upper()
|
| 1058 |
+
predictor = re.sub(r"\s+", " ", str(item.get("predictor") or "")).strip()
|
| 1059 |
+
outcome = re.sub(r"\s+", " ", str(item.get("outcome") or "")).strip()
|
| 1060 |
+
if hid and predictor and outcome:
|
| 1061 |
+
lines.append(f"{hid}: {predictor} is associated with {outcome}.")
|
| 1062 |
+
return "\n".join(lines)
|
| 1063 |
+
|
| 1064 |
+
@staticmethod
|
| 1065 |
+
def _normalize_hypothesis_ordinal_aliases(text: str, ledger: dict) -> str:
|
| 1066 |
+
"""Map safe ordinal aliases to frozen H-identifiers without semantic rewrite."""
|
| 1067 |
+
value = str(text or "")
|
| 1068 |
+
items = [item for item in (dict(ledger or {}).get("items") or []) if isinstance(item, dict)]
|
| 1069 |
+
if not items:
|
| 1070 |
+
return value
|
| 1071 |
+
words = ["first", "second", "third", "fourth", "fifth", "sixth"]
|
| 1072 |
+
for index, item in enumerate(items[: len(words)]):
|
| 1073 |
+
hid = str(item.get("hypothesis_id") or "").strip().upper()
|
| 1074 |
+
if not hid:
|
| 1075 |
+
continue
|
| 1076 |
+
word = words[index]
|
| 1077 |
+
value = re.sub(rf"\b(?:the\s+)?{word}\s+hypothesis\b", hid, value, flags=re.I)
|
| 1078 |
+
return value
|
| 1079 |
+
|
| 1080 |
@classmethod
|
| 1081 |
def _hypothesis_contract_violations(
|
| 1082 |
cls, text: str, ledger: dict, *, section_name: str = "", require_all: bool = False
|
|
|
|
| 1111 |
"reason": "Ordinal hypothesis aliases can silently reorder the canonical H1-Hn identity.",
|
| 1112 |
"repair_instruction": "Refer to hypotheses only by their frozen H1-Hn identifiers.",
|
| 1113 |
})
|
| 1114 |
+
all_constructs = {
|
| 1115 |
+
re.sub(r"\s+", " ", str(item.get(key) or "")).strip()
|
| 1116 |
+
for item in items for key in ("predictor", "outcome")
|
| 1117 |
+
if str(item.get(key) or "").strip()
|
| 1118 |
+
}
|
| 1119 |
+
for start, end, sentence in SSciPipelineAgent._sentence_spans(value):
|
| 1120 |
+
ids = list(dict.fromkeys(
|
| 1121 |
+
f"H{n}" for n in re.findall(r"\bH(\d{1,2})\b", sentence, flags=re.I)
|
| 1122 |
+
))
|
| 1123 |
+
# Collective references such as "H1-H3 are summarized below"
|
| 1124 |
+
# are not definitions and must not be mistaken for remapping.
|
| 1125 |
+
if len(ids) != 1:
|
| 1126 |
+
continue
|
| 1127 |
+
hid = ids[0]
|
| 1128 |
+
item = expected.get(hid)
|
| 1129 |
+
if not item:
|
| 1130 |
+
continue
|
| 1131 |
+
predictor = re.sub(r"\s+", " ", str(item.get("predictor") or "")).strip()
|
| 1132 |
+
outcome = re.sub(r"\s+", " ", str(item.get("outcome") or "")).strip()
|
| 1133 |
+
lower_sentence = re.sub(r"\s+", " ", sentence).casefold()
|
| 1134 |
+
mentioned_constructs = {
|
| 1135 |
+
term for term in all_constructs if term.casefold() in lower_sentence
|
| 1136 |
}
|
| 1137 |
+
# Pairing identity is global, not limited to the theory-definition
|
| 1138 |
+
# sections. Require at least two named constructs so a bare reference
|
| 1139 |
+
# such as "H1 was supported" cannot be misclassified as remapping.
|
| 1140 |
+
if len(mentioned_constructs) >= 2 and predictor and outcome and (
|
| 1141 |
+
predictor.casefold() not in lower_sentence or outcome.casefold() not in lower_sentence
|
| 1142 |
+
):
|
| 1143 |
+
rows.append({
|
| 1144 |
+
"type": "hypothesis_contract_pairing_mismatch",
|
| 1145 |
+
"id": hid,
|
| 1146 |
+
"sentence": re.sub(r"\s+", " ", sentence).strip()[:420],
|
| 1147 |
+
"expected": f"{predictor} -> {outcome}",
|
| 1148 |
+
"reason": "A canonical hypothesis label was attached to a different construct pairing.",
|
| 1149 |
+
"repair_instruction": f"Define {hid} only for {predictor} and {outcome}, using association language.",
|
| 1150 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1151 |
if require_all:
|
| 1152 |
missing = sorted(set(expected) - observed)
|
| 1153 |
if missing:
|
|
|
|
| 2476 |
)
|
| 2477 |
state["data_metadata"] = data_metadata
|
| 2478 |
|
| 2479 |
+
hypothesis_ledger = dict(data_metadata.get("hypothesis_ledger") or {}) if mode == "amj" else {}
|
| 2480 |
+
hypothesis_contract = self._hypothesis_contract_directive(hypothesis_ledger)
|
| 2481 |
+
if hypothesis_ledger:
|
| 2482 |
+
# C5.1 single authority: legacy prose extraction may not redefine AMJ
|
| 2483 |
+
# H1-Hn once the result-independent ledger exists. Downstream memory is
|
| 2484 |
+
# populated deterministically from that ledger and survives checkpoints.
|
| 2485 |
+
agent.global_memory["hypotheses"] = self._hypothesis_memory_text(hypothesis_ledger)
|
| 2486 |
+
agent.global_memory["hypothesis_contract_locked"] = True
|
| 2487 |
+
|
| 2488 |
self._qualitative_quote_sources = (
|
| 2489 |
self._qualitative_quote_source_map(data_metadata) if mode == "asq" else {}
|
| 2490 |
)
|
|
|
|
| 2559 |
agent.stream_generate_research_logic(
|
| 2560 |
topic, keywords, synthesis_result, refs_text,
|
| 2561 |
data_evidence=planning_data_md, mode=mode,
|
| 2562 |
+
hypothesis_contract=hypothesis_contract,
|
| 2563 |
)
|
| 2564 |
)
|
| 2565 |
agent.record_prewrite_stage("research_logic", prewrite_started, prewrite_calls)
|
| 2566 |
research_logic_contract = re.sub(
|
| 2567 |
r"<think>.*?</think>", "", research_logic_response, flags=re.DOTALL
|
| 2568 |
).strip()
|
| 2569 |
+
if hypothesis_ledger:
|
| 2570 |
+
research_logic_contract = self._normalize_hypothesis_ordinal_aliases(
|
| 2571 |
+
research_logic_contract, hypothesis_ledger
|
| 2572 |
+
)
|
| 2573 |
+
logic_hypothesis_rows = self._hypothesis_contract_violations(
|
| 2574 |
+
research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract"
|
| 2575 |
+
)
|
| 2576 |
+
if logic_hypothesis_rows:
|
| 2577 |
+
raise RuntimeError(
|
| 2578 |
+
f"research_logic_hypothesis_contract_invalid:{logic_hypothesis_rows[:3]}"
|
| 2579 |
+
)
|
| 2580 |
if not research_logic_contract:
|
| 2581 |
raise RuntimeError("research_logic_contract_missing")
|
| 2582 |
agent.global_memory["research_logic_contract"] = research_logic_contract
|
|
|
|
| 2585 |
working_front = agent.generate_working_front_matter(
|
| 2586 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2587 |
data_evidence=planning_data_md, mode=mode,
|
| 2588 |
+
hypothesis_contract=hypothesis_contract,
|
| 2589 |
)
|
| 2590 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 2591 |
+
if hypothesis_ledger:
|
| 2592 |
+
working_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 2593 |
+
working_abstract, hypothesis_ledger
|
| 2594 |
+
)
|
| 2595 |
working_keywords = list(working_front.get("keywords") or [])
|
| 2596 |
if not working_abstract or not working_keywords:
|
| 2597 |
raise RuntimeError("working_front_matter_missing")
|
|
|
|
| 2599 |
mode, agent, quant_engine, working_abstract, data_metadata,
|
| 2600 |
section_name="Working Abstract",
|
| 2601 |
)
|
| 2602 |
+
if hypothesis_ledger:
|
| 2603 |
+
front_fact_rows.extend(self._hypothesis_contract_violations(
|
| 2604 |
+
working_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 2605 |
+
))
|
| 2606 |
if front_fact_rows:
|
| 2607 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 2608 |
logging.warning(
|
|
|
|
| 2612 |
working_front = agent.generate_working_front_matter(
|
| 2613 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2614 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 2615 |
+
hypothesis_contract=hypothesis_contract,
|
| 2616 |
)
|
| 2617 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 2618 |
+
if hypothesis_ledger:
|
| 2619 |
+
working_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 2620 |
+
working_abstract, hypothesis_ledger
|
| 2621 |
+
)
|
| 2622 |
working_keywords = list(working_front.get("keywords") or [])
|
| 2623 |
_, remaining_front_rows = self._repair_validate_study_fact_contract(
|
| 2624 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 2625 |
)
|
| 2626 |
+
if hypothesis_ledger:
|
| 2627 |
+
remaining_front_rows.extend(self._hypothesis_contract_violations(
|
| 2628 |
+
working_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 2629 |
+
))
|
| 2630 |
if remaining_front_rows:
|
| 2631 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining_front_rows[:3]}")
|
| 2632 |
quality_diagnostics.append({
|
|
|
|
| 2824 |
research_logic_contract = str(
|
| 2825 |
state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
|
| 2826 |
)
|
| 2827 |
+
if hypothesis_ledger and research_logic_contract:
|
| 2828 |
+
research_logic_contract = self._normalize_hypothesis_ordinal_aliases(
|
| 2829 |
+
research_logic_contract, hypothesis_ledger
|
| 2830 |
+
)
|
| 2831 |
+
resumed_logic_rows = self._hypothesis_contract_violations(
|
| 2832 |
+
research_logic_contract, hypothesis_ledger, section_name="Research Logic Contract"
|
| 2833 |
+
)
|
| 2834 |
+
if resumed_logic_rows:
|
| 2835 |
+
raise RuntimeError(
|
| 2836 |
+
f"research_logic_hypothesis_contract_invalid_on_resume:{resumed_logic_rows[:3]}"
|
| 2837 |
+
)
|
| 2838 |
+
agent.global_memory["research_logic_contract"] = research_logic_contract
|
| 2839 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 2840 |
citation_minimums = dict(state.get("citation_minimums") or {})
|
| 2841 |
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
|
|
|
|
| 2877 |
working_abstract = str(state.get("working_abstract") or "").strip()
|
| 2878 |
working_keywords = list(state.get("working_keywords") or [])
|
| 2879 |
if working_abstract and not state.get("english_finalized"):
|
| 2880 |
+
if hypothesis_ledger:
|
| 2881 |
+
working_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 2882 |
+
working_abstract, hypothesis_ledger
|
| 2883 |
+
)
|
| 2884 |
_, resumed_front_rows = self._repair_validate_study_fact_contract(
|
| 2885 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 2886 |
)
|
| 2887 |
+
if hypothesis_ledger:
|
| 2888 |
+
resumed_front_rows.extend(self._hypothesis_contract_violations(
|
| 2889 |
+
working_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 2890 |
+
))
|
| 2891 |
if resumed_front_rows:
|
| 2892 |
feedback = self._section_retry_feedback(resumed_front_rows)
|
| 2893 |
working_front = agent.generate_working_front_matter(
|
| 2894 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 2895 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 2896 |
+
hypothesis_contract=hypothesis_contract,
|
| 2897 |
)
|
| 2898 |
candidate_abstract = str(working_front.get("abstract") or "").strip()
|
| 2899 |
+
if hypothesis_ledger:
|
| 2900 |
+
candidate_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 2901 |
+
candidate_abstract, hypothesis_ledger
|
| 2902 |
+
)
|
| 2903 |
candidate_keywords = list(working_front.get("keywords") or [])
|
| 2904 |
_, remaining = self._repair_validate_study_fact_contract(
|
| 2905 |
mode, agent, quant_engine, candidate_abstract, data_metadata, section_name="Working Abstract",
|
| 2906 |
)
|
| 2907 |
+
if hypothesis_ledger:
|
| 2908 |
+
remaining.extend(self._hypothesis_contract_violations(
|
| 2909 |
+
candidate_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 2910 |
+
))
|
| 2911 |
if remaining:
|
| 2912 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining[:3]}")
|
| 2913 |
working_abstract = candidate_abstract
|
|
|
|
| 3106 |
working_front = agent.generate_working_front_matter(
|
| 3107 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 3108 |
data_evidence=planning_data_md, mode=mode,
|
| 3109 |
+
hypothesis_contract=hypothesis_contract,
|
| 3110 |
)
|
| 3111 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 3112 |
+
if hypothesis_ledger:
|
| 3113 |
+
working_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 3114 |
+
working_abstract, hypothesis_ledger
|
| 3115 |
+
)
|
| 3116 |
working_keywords = list(working_front.get("keywords") or [])
|
| 3117 |
if not working_abstract or not working_keywords:
|
| 3118 |
raise RuntimeError("working_front_matter_missing")
|
| 3119 |
_, front_fact_rows = self._repair_validate_study_fact_contract(
|
| 3120 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 3121 |
)
|
| 3122 |
+
if hypothesis_ledger:
|
| 3123 |
+
front_fact_rows.extend(self._hypothesis_contract_violations(
|
| 3124 |
+
working_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 3125 |
+
))
|
| 3126 |
if front_fact_rows:
|
| 3127 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 3128 |
working_front = agent.generate_working_front_matter(
|
| 3129 |
topic, keywords, research_logic_contract, synthesis_result,
|
| 3130 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 3131 |
+
hypothesis_contract=hypothesis_contract,
|
| 3132 |
)
|
| 3133 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 3134 |
+
if hypothesis_ledger:
|
| 3135 |
+
working_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 3136 |
+
working_abstract, hypothesis_ledger
|
| 3137 |
+
)
|
| 3138 |
working_keywords = list(working_front.get("keywords") or [])
|
| 3139 |
_, remaining = self._repair_validate_study_fact_contract(
|
| 3140 |
mode, agent, quant_engine, working_abstract, data_metadata, section_name="Working Abstract",
|
| 3141 |
)
|
| 3142 |
+
if hypothesis_ledger:
|
| 3143 |
+
remaining.extend(self._hypothesis_contract_violations(
|
| 3144 |
+
working_abstract, hypothesis_ledger, section_name="Working Abstract"
|
| 3145 |
+
))
|
| 3146 |
if remaining:
|
| 3147 |
raise RuntimeError(f"working_front_matter_study_fact_invalid_after_retry:{remaining[:3]}")
|
| 3148 |
quality_diagnostics.append({
|
|
|
|
| 3528 |
en_raw = re.sub(r"(?m)^(#{2,3})\s+[\d\.\s一二三四五六七八九十、]+\s*(.*)", r"\1 \2", en_raw)
|
| 3529 |
en_raw = re.sub(r"(?m)^#{4,}\s+(.*)", r"**\1**", en_raw)
|
| 3530 |
en_raw = re.sub(r"(?m)^#\s+(.+)$", r"## \1", en_raw)
|
| 3531 |
+
if mode == "amj" and hypothesis_ledger:
|
| 3532 |
+
normalized_hypothesis_text = self._normalize_hypothesis_ordinal_aliases(
|
| 3533 |
+
en_raw, hypothesis_ledger
|
| 3534 |
+
)
|
| 3535 |
+
if normalized_hypothesis_text != en_raw:
|
| 3536 |
+
logging.info(
|
| 3537 |
+
"Deterministic hypothesis ordinal alias normalization | unit=%s",
|
| 3538 |
+
display_name,
|
| 3539 |
+
)
|
| 3540 |
+
en_raw = normalized_hypothesis_text
|
| 3541 |
if mode == "asq":
|
| 3542 |
# Model prose has no authority over quote presence/count.
|
| 3543 |
en_raw = self._strip_model_controlled_qualitative_quotes(en_raw, data_metadata)
|
|
|
|
| 4043 |
# front matter deterministically. No body rebalance may occur afterward.
|
| 4044 |
self._progress(92, "final_front_matter")
|
| 4045 |
final_front = agent.generate_final_front_matter(
|
| 4046 |
+
topic, keywords, body_only, data_evidence=user_data_md, mode=mode,
|
| 4047 |
+
hypothesis_contract=hypothesis_contract,
|
| 4048 |
)
|
| 4049 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
| 4050 |
+
if hypothesis_ledger:
|
| 4051 |
+
final_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 4052 |
+
final_abstract, hypothesis_ledger
|
| 4053 |
+
)
|
| 4054 |
final_keywords = list(final_front.get("keywords") or [])
|
| 4055 |
if not final_abstract or not final_keywords:
|
| 4056 |
raise RuntimeError("final_front_matter_missing")
|
|
|
|
| 4058 |
mode, agent, quant_engine, final_abstract, data_metadata,
|
| 4059 |
section_name="Final Abstract",
|
| 4060 |
)
|
| 4061 |
+
if hypothesis_ledger:
|
| 4062 |
+
final_front_fact_rows.extend(self._hypothesis_contract_violations(
|
| 4063 |
+
final_abstract, hypothesis_ledger, section_name="Final Abstract"
|
| 4064 |
+
))
|
| 4065 |
if final_front_fact_rows:
|
| 4066 |
feedback = self._section_retry_feedback(final_front_fact_rows)
|
| 4067 |
logging.warning(
|
|
|
|
| 4071 |
)
|
| 4072 |
final_front = agent.generate_final_front_matter(
|
| 4073 |
topic, keywords, body_only, data_evidence=user_data_md, mode=mode,
|
| 4074 |
+
validation_feedback=feedback, hypothesis_contract=hypothesis_contract,
|
| 4075 |
)
|
| 4076 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
| 4077 |
+
if hypothesis_ledger:
|
| 4078 |
+
final_abstract = self._normalize_hypothesis_ordinal_aliases(
|
| 4079 |
+
final_abstract, hypothesis_ledger
|
| 4080 |
+
)
|
| 4081 |
final_keywords = list(final_front.get("keywords") or [])
|
| 4082 |
if not final_abstract or not final_keywords:
|
| 4083 |
raise RuntimeError("final_front_matter_missing_after_retry")
|
|
|
|
| 4085 |
mode, agent, quant_engine, final_abstract, data_metadata,
|
| 4086 |
section_name="Final Abstract",
|
| 4087 |
)
|
| 4088 |
+
if hypothesis_ledger:
|
| 4089 |
+
remaining_final_fact_rows.extend(self._hypothesis_contract_violations(
|
| 4090 |
+
final_abstract, hypothesis_ledger, section_name="Final Abstract"
|
| 4091 |
+
))
|
| 4092 |
if remaining_final_fact_rows:
|
| 4093 |
state["final_abstract_retry_exhausted"] = {
|
| 4094 |
"attempts": 2,
|
|
|
|
| 4114 |
state.pop("final_abstract_retry_exhausted", None)
|
| 4115 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 4116 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
| 4117 |
+
if mode == "amj" and hypothesis_ledger:
|
| 4118 |
+
post_front_hypothesis_rows = self._hypothesis_contract_violations(
|
| 4119 |
+
full_draft_en, hypothesis_ledger, section_name="Full Manuscript", require_all=True
|
| 4120 |
+
)
|
| 4121 |
+
if post_front_hypothesis_rows:
|
| 4122 |
+
raise RuntimeError(
|
| 4123 |
+
f"final_hypothesis_contract_failed_after_front_matter:{post_front_hypothesis_rows[:3]}"
|
| 4124 |
+
)
|
| 4125 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 4126 |
full_draft_en, minimum_references=final_reference_count
|
| 4127 |
)
|
core/pipeline_agent.py
CHANGED
|
@@ -195,7 +195,7 @@ class SSciPipelineAgent:
|
|
| 195 |
self.global_memory = {
|
| 196 |
"hypotheses": "", "variables": "", "model_summary": "",
|
| 197 |
"method_summary": "", "research_logic_contract": "",
|
| 198 |
-
"core_golden_metadata": []
|
| 199 |
}
|
| 200 |
|
| 201 |
try:
|
|
@@ -456,7 +456,10 @@ class SSciPipelineAgent:
|
|
| 456 |
|
| 457 |
def _extract_memory_from_text(self, text, section_name):
|
| 458 |
lower_sec = section_name.lower()
|
| 459 |
-
if
|
|
|
|
|
|
|
|
|
|
| 460 |
hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
|
| 461 |
if hypo_matches:
|
| 462 |
self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
|
|
@@ -1274,6 +1277,7 @@ class SSciPipelineAgent:
|
|
| 1274 |
refs_text,
|
| 1275 |
data_evidence="",
|
| 1276 |
mode="amj",
|
|
|
|
| 1277 |
):
|
| 1278 |
"""Generate a provisional research-logic contract without prewriting results."""
|
| 1279 |
mode = 'asq' if str(mode).lower() == 'asq' else 'amj'
|
|
@@ -1296,6 +1300,10 @@ Literature Synthesis:
|
|
| 1296 |
|
| 1297 |
{evidence_block}
|
| 1298 |
{mode_directive}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1299 |
Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It must guide later sections without fixing an expected result or conclusion. Do not use citations in the contract."""
|
| 1300 |
def _stream_direct_research_logic():
|
| 1301 |
# R4-A: research logic is a concise planning contract. A reasoning-first
|
|
@@ -1469,7 +1477,7 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1469 |
raise DeepSeekRequestError(f'{phase} front matter validation failed: {last_error}')
|
| 1470 |
|
| 1471 |
def generate_working_front_matter(
|
| 1472 |
-
self, topic, keywords, research_logic_contract, synthesis_result, data_evidence='', mode='amj', validation_feedback=''
|
| 1473 |
):
|
| 1474 |
"""Create a provisional visible abstract/keyword anchor without prewriting results."""
|
| 1475 |
evidence = self._truncate_context(str(data_evidence or ''), 3500)
|
|
@@ -1490,6 +1498,10 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1490 |
<FROZEN_RESEARCH_EVIDENCE>
|
| 1491 |
{evidence}
|
| 1492 |
</FROZEN_RESEARCH_EVIDENCE>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
<LOCAL_VALIDATION_FEEDBACK>
|
| 1494 |
{str(validation_feedback or '')[:2500]}
|
| 1495 |
</LOCAL_VALIDATION_FEEDBACK>"""
|
|
@@ -1503,7 +1515,7 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1503 |
prompt, phase='Working', max_tokens=1100, validator=validate_working,
|
| 1504 |
)
|
| 1505 |
|
| 1506 |
-
def generate_final_front_matter(self, topic, keywords, manuscript, data_evidence='', mode='amj', validation_feedback=''):
|
| 1507 |
"""Generate final Abstract + Keywords only from the frozen completed body/evidence."""
|
| 1508 |
evidence = self._truncate_context(str(data_evidence or ''), 5000)
|
| 1509 |
body = self._abstract_manuscript_context(manuscript, 18000)
|
|
@@ -1516,6 +1528,10 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1516 |
<FROZEN_RESEARCH_EVIDENCE>
|
| 1517 |
{evidence}
|
| 1518 |
</FROZEN_RESEARCH_EVIDENCE>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1519 |
<COMPLETED_FROZEN_MANUSCRIPT>
|
| 1520 |
{body}
|
| 1521 |
</COMPLETED_FROZEN_MANUSCRIPT>
|
|
@@ -2585,7 +2601,7 @@ Return ONLY a valid JSON array of strings."""
|
|
| 2585 |
return int(match.group()) if match else 10**9
|
| 2586 |
|
| 2587 |
@staticmethod
|
| 2588 |
-
def
|
| 2589 |
stopwords = {
|
| 2590 |
"about", "after", "also", "among", "and", "are", "because",
|
| 2591 |
"been", "before", "between", "both", "can", "could", "does",
|
|
@@ -2596,7 +2612,11 @@ Return ONLY a valid JSON array of strings."""
|
|
| 2596 |
"with", "within", "would",
|
| 2597 |
}
|
| 2598 |
words = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", str(value).lower())
|
| 2599 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2600 |
|
| 2601 |
def build_claim_evidence_plan(self, tasks, topic, synthesis_result, mode='amj', target_unique=60):
|
| 2602 |
"""Build a bounded citation-first plan without one oversized model call.
|
|
@@ -2810,6 +2830,17 @@ INPUT:
|
|
| 2810 |
'use','uses','used','using','user','users','approach','approaches','framework','frameworks',
|
| 2811 |
})
|
| 2812 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2813 |
@classmethod
|
| 2814 |
def _citation_recovery_overlap(cls, section_name, section_context, claim, evidence):
|
| 2815 |
anchor_terms = cls._citation_terms(f"{section_name} {section_context}")
|
|
@@ -2941,30 +2972,62 @@ INPUT:
|
|
| 2941 |
return selected
|
| 2942 |
|
| 2943 |
def _deterministic_contextual_jit_query(self, topic, section_name, section_context):
|
| 2944 |
-
"""Build one bounded
|
| 2945 |
-
|
| 2946 |
-
|
| 2947 |
-
|
| 2948 |
-
|
| 2949 |
-
|
| 2950 |
-
|
| 2951 |
-
|
| 2952 |
-
|
| 2953 |
-
|
| 2954 |
-
|
| 2955 |
-
|
| 2956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2957 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2958 |
selected = []
|
| 2959 |
-
#
|
| 2960 |
-
#
|
| 2961 |
-
|
| 2962 |
-
|
| 2963 |
-
if
|
| 2964 |
-
selected.append(
|
|
|
|
|
|
|
|
|
|
| 2965 |
if len(selected) >= 5:
|
| 2966 |
break
|
| 2967 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2968 |
|
| 2969 |
def fetch_contextual_jit_recovery_items(
|
| 2970 |
self, *, topic, section_name, section_context, section_id='', needed=1, excluded_tags=None,
|
|
@@ -4170,6 +4233,62 @@ INPUT:
|
|
| 4170 |
core = f"{core} {tag}{punctuation}".strip()
|
| 4171 |
return f"{leading}{core}{trailing}"
|
| 4172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4173 |
def semantic_audit_claim_specific_citations(
|
| 4174 |
self,
|
| 4175 |
text,
|
|
@@ -4391,23 +4510,30 @@ Return exactly:
|
|
| 4391 |
)
|
| 4392 |
continue
|
| 4393 |
if status == "split":
|
| 4394 |
-
#
|
| 4395 |
-
#
|
| 4396 |
-
#
|
| 4397 |
-
#
|
| 4398 |
-
#
|
| 4399 |
-
#
|
| 4400 |
if len(revised_tags) == 1:
|
| 4401 |
selected = next(iter(revised_tags))
|
| 4402 |
-
|
| 4403 |
-
record["original"], selected
|
|
|
|
|
|
|
| 4404 |
)
|
| 4405 |
-
|
| 4406 |
-
|
| 4407 |
-
|
| 4408 |
-
|
| 4409 |
-
|
| 4410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4411 |
violation = {
|
| 4412 |
"type":"citation_claim_split_required", "id":record["id"],
|
| 4413 |
"sentence":record["original"][:500], "reason":reason,
|
|
|
|
| 195 |
self.global_memory = {
|
| 196 |
"hypotheses": "", "variables": "", "model_summary": "",
|
| 197 |
"method_summary": "", "research_logic_contract": "",
|
| 198 |
+
"core_golden_metadata": [], "hypothesis_contract_locked": False,
|
| 199 |
}
|
| 200 |
|
| 201 |
try:
|
|
|
|
| 456 |
|
| 457 |
def _extract_memory_from_text(self, text, section_name):
|
| 458 |
lower_sec = section_name.lower()
|
| 459 |
+
if (
|
| 460 |
+
not bool(self.global_memory.get("hypothesis_contract_locked"))
|
| 461 |
+
and any(kw in lower_sec for kw in ["introduction", "literature", "hypothesis", "framing"])
|
| 462 |
+
):
|
| 463 |
hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
|
| 464 |
if hypo_matches:
|
| 465 |
self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
|
|
|
|
| 1277 |
refs_text,
|
| 1278 |
data_evidence="",
|
| 1279 |
mode="amj",
|
| 1280 |
+
hypothesis_contract="",
|
| 1281 |
):
|
| 1282 |
"""Generate a provisional research-logic contract without prewriting results."""
|
| 1283 |
mode = 'asq' if str(mode).lower() == 'asq' else 'amj'
|
|
|
|
| 1300 |
|
| 1301 |
{evidence_block}
|
| 1302 |
{mode_directive}
|
| 1303 |
+
<IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1304 |
+
{str(hypothesis_contract or '')}
|
| 1305 |
+
</IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1306 |
+
If the immutable hypothesis contract is non-empty, it is the sole authority for H-identifiers and construct pairings. Do not redefine, reorder, or infer an alternative H1-Hn architecture.
|
| 1307 |
Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It must guide later sections without fixing an expected result or conclusion. Do not use citations in the contract."""
|
| 1308 |
def _stream_direct_research_logic():
|
| 1309 |
# R4-A: research logic is a concise planning contract. A reasoning-first
|
|
|
|
| 1477 |
raise DeepSeekRequestError(f'{phase} front matter validation failed: {last_error}')
|
| 1478 |
|
| 1479 |
def generate_working_front_matter(
|
| 1480 |
+
self, topic, keywords, research_logic_contract, synthesis_result, data_evidence='', mode='amj', validation_feedback='', hypothesis_contract=''
|
| 1481 |
):
|
| 1482 |
"""Create a provisional visible abstract/keyword anchor without prewriting results."""
|
| 1483 |
evidence = self._truncate_context(str(data_evidence or ''), 3500)
|
|
|
|
| 1498 |
<FROZEN_RESEARCH_EVIDENCE>
|
| 1499 |
{evidence}
|
| 1500 |
</FROZEN_RESEARCH_EVIDENCE>
|
| 1501 |
+
<IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1502 |
+
{str(hypothesis_contract or '')}
|
| 1503 |
+
</IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1504 |
+
If the immutable hypothesis contract is non-empty, preserve its H-identifiers and construct pairings exactly; do not invent or reorder hypotheses.
|
| 1505 |
<LOCAL_VALIDATION_FEEDBACK>
|
| 1506 |
{str(validation_feedback or '')[:2500]}
|
| 1507 |
</LOCAL_VALIDATION_FEEDBACK>"""
|
|
|
|
| 1515 |
prompt, phase='Working', max_tokens=1100, validator=validate_working,
|
| 1516 |
)
|
| 1517 |
|
| 1518 |
+
def generate_final_front_matter(self, topic, keywords, manuscript, data_evidence='', mode='amj', validation_feedback='', hypothesis_contract=''):
|
| 1519 |
"""Generate final Abstract + Keywords only from the frozen completed body/evidence."""
|
| 1520 |
evidence = self._truncate_context(str(data_evidence or ''), 5000)
|
| 1521 |
body = self._abstract_manuscript_context(manuscript, 18000)
|
|
|
|
| 1528 |
<FROZEN_RESEARCH_EVIDENCE>
|
| 1529 |
{evidence}
|
| 1530 |
</FROZEN_RESEARCH_EVIDENCE>
|
| 1531 |
+
<IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1532 |
+
{str(hypothesis_contract or '')}
|
| 1533 |
+
</IMMUTABLE_HYPOTHESIS_CONTRACT>
|
| 1534 |
+
If the immutable hypothesis contract is non-empty, preserve its H-identifiers and construct pairings exactly; do not invent or reorder hypotheses.
|
| 1535 |
<COMPLETED_FROZEN_MANUSCRIPT>
|
| 1536 |
{body}
|
| 1537 |
</COMPLETED_FROZEN_MANUSCRIPT>
|
|
|
|
| 2601 |
return int(match.group()) if match else 10**9
|
| 2602 |
|
| 2603 |
@staticmethod
|
| 2604 |
+
def _citation_term_sequence(value):
|
| 2605 |
stopwords = {
|
| 2606 |
"about", "after", "also", "among", "and", "are", "because",
|
| 2607 |
"been", "before", "between", "both", "can", "could", "does",
|
|
|
|
| 2612 |
"with", "within", "would",
|
| 2613 |
}
|
| 2614 |
words = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", str(value).lower())
|
| 2615 |
+
return [word for word in words if word not in stopwords]
|
| 2616 |
+
|
| 2617 |
+
@classmethod
|
| 2618 |
+
def _citation_terms(cls, value):
|
| 2619 |
+
return set(cls._citation_term_sequence(value))
|
| 2620 |
|
| 2621 |
def build_claim_evidence_plan(self, tasks, topic, synthesis_result, mode='amj', target_unique=60):
|
| 2622 |
"""Build a bounded citation-first plan without one oversized model call.
|
|
|
|
| 2830 |
'use','uses','used','using','user','users','approach','approaches','framework','frameworks',
|
| 2831 |
})
|
| 2832 |
|
| 2833 |
+
_CITATION_RECOVERY_QUERY_NOISE_TERMS = frozenset({
|
| 2834 |
+
'academic','academics','accept','accepted','acceptance','acceptable','access','accessed',
|
| 2835 |
+
'account','accounts','absence','absent','abandoned','accomplish','accomplished','achieve',
|
| 2836 |
+
'achieved','achieves','across','accordingly','additional','aim','aims','allow','allows',
|
| 2837 |
+
'available','based','basis','consider','considered','current','different','evidence',
|
| 2838 |
+
'existing','finding','findings','important','indicate','indicates','paper','prior','process',
|
| 2839 |
+
'processes','provide','provides','related','relationship','relationships','relevant','report',
|
| 2840 |
+
'reports','show','shows','specific','support','supports','introduction','discussion',
|
| 2841 |
+
'background','theoretical','framing','methodology','setting','results','hypothesis','hypotheses',
|
| 2842 |
+
})
|
| 2843 |
+
|
| 2844 |
@classmethod
|
| 2845 |
def _citation_recovery_overlap(cls, section_name, section_context, claim, evidence):
|
| 2846 |
anchor_terms = cls._citation_terms(f"{section_name} {section_context}")
|
|
|
|
| 2972 |
return selected
|
| 2973 |
|
| 2974 |
def _deterministic_contextual_jit_query(self, topic, section_name, section_context):
|
| 2975 |
+
"""Build one bounded topic/construct-first recovery query without a model call.
|
| 2976 |
+
|
| 2977 |
+
C5.1 deliberately preserves semantic/source order. The previous code
|
| 2978 |
+
converted the manuscript context to a set and then alphabetically sorted
|
| 2979 |
+
it, which produced queries such as ``academic accept access ...``. Topic
|
| 2980 |
+
terms now anchor the query, while context terms are ranked by occurrence
|
| 2981 |
+
frequency with first appearance as the deterministic tie-breaker.
|
| 2982 |
+
"""
|
| 2983 |
+
noise = set(self._CITATION_RECOVERY_GENERIC_TERMS) | set(self._CITATION_RECOVERY_QUERY_NOISE_TERMS)
|
| 2984 |
+
|
| 2985 |
+
def clean_sequence(value):
|
| 2986 |
+
output = []
|
| 2987 |
+
for term in self._citation_term_sequence(value):
|
| 2988 |
+
if len(term) < 3 or term in noise or term in output:
|
| 2989 |
+
continue
|
| 2990 |
+
output.append(term)
|
| 2991 |
+
return output
|
| 2992 |
+
|
| 2993 |
+
topic_terms = clean_sequence(topic)
|
| 2994 |
+
section_terms = clean_sequence(section_name)
|
| 2995 |
+
raw_context = [
|
| 2996 |
+
term for term in self._citation_term_sequence(section_context)
|
| 2997 |
+
if len(term) >= 3 and term not in noise
|
| 2998 |
]
|
| 2999 |
+
counts = Counter(raw_context)
|
| 3000 |
+
first_index = {}
|
| 3001 |
+
for index, term in enumerate(raw_context):
|
| 3002 |
+
first_index.setdefault(term, index)
|
| 3003 |
+
context_terms = sorted(
|
| 3004 |
+
counts,
|
| 3005 |
+
key=lambda term: (-counts[term], first_index[term]),
|
| 3006 |
+
)
|
| 3007 |
+
|
| 3008 |
selected = []
|
| 3009 |
+
# A recovery query should remain anchored in the manuscript topic rather
|
| 3010 |
+
# than drifting toward alphabetically early prose vocabulary.
|
| 3011 |
+
topic_target = min(3, len(topic_terms))
|
| 3012 |
+
for term in topic_terms[:topic_target]:
|
| 3013 |
+
if term not in selected:
|
| 3014 |
+
selected.append(term)
|
| 3015 |
+
for term in section_terms + context_terms + topic_terms[topic_target:]:
|
| 3016 |
+
if term not in selected:
|
| 3017 |
+
selected.append(term)
|
| 3018 |
if len(selected) >= 5:
|
| 3019 |
break
|
| 3020 |
+
|
| 3021 |
+
# When two or more topic/construct-specific terms are available, at
|
| 3022 |
+
# least two must survive into the bounded query. Otherwise skip JIT
|
| 3023 |
+
# instead of spending retrieval budget on a semantically weak query.
|
| 3024 |
+
if len(topic_terms) >= 2 and len(set(selected) & set(topic_terms)) < 2:
|
| 3025 |
+
return ''
|
| 3026 |
+
if len(selected) < 3:
|
| 3027 |
+
return ''
|
| 3028 |
+
if any(term in noise for term in selected):
|
| 3029 |
+
return ''
|
| 3030 |
+
return ' '.join(selected[:5])
|
| 3031 |
|
| 3032 |
def fetch_contextual_jit_recovery_items(
|
| 3033 |
self, *, topic, section_name, section_context, section_id='', needed=1, excluded_tags=None,
|
|
|
|
| 4233 |
core = f"{core} {tag}{punctuation}".strip()
|
| 4234 |
return f"{leading}{core}{trailing}"
|
| 4235 |
|
| 4236 |
+
def _adjudicate_single_source_split(self, *, sentence, selected_tag, evidence, split_reason, section_name=''):
|
| 4237 |
+
"""Decide whether one selected source supports the complete compound claim.
|
| 4238 |
+
|
| 4239 |
+
This is a bounded decision-only semantic check. It has no prose rewrite,
|
| 4240 |
+
citation invention, or retrieval authority. Any malformed/failed decision
|
| 4241 |
+
fails closed as partial support so the existing same-Section retry owns the
|
| 4242 |
+
correction.
|
| 4243 |
+
"""
|
| 4244 |
+
selected_evidence = ''
|
| 4245 |
+
selected_reference = ''
|
| 4246 |
+
for row in evidence or []:
|
| 4247 |
+
if str(row.get('tag') or '').strip() == str(selected_tag or '').strip():
|
| 4248 |
+
selected_evidence = str(row.get('evidence') or '').strip()
|
| 4249 |
+
selected_reference = str(row.get('reference') or '').strip()
|
| 4250 |
+
break
|
| 4251 |
+
payload = {
|
| 4252 |
+
'section': str(section_name or ''),
|
| 4253 |
+
'sentence': str(sentence or ''),
|
| 4254 |
+
'selected_tag': str(selected_tag or ''),
|
| 4255 |
+
'selected_reference': selected_reference,
|
| 4256 |
+
'selected_evidence': selected_evidence,
|
| 4257 |
+
'original_split_reason': str(split_reason or '')[:700],
|
| 4258 |
+
}
|
| 4259 |
+
system_prompt = (
|
| 4260 |
+
"You are a strict citation-support adjudicator. Decide only whether the supplied single source directly supports "
|
| 4261 |
+
"the complete original sentence, including every material clause. Do not rewrite prose, add citations, infer from title alone, "
|
| 4262 |
+
"or use outside knowledge. Return JSON only: "
|
| 4263 |
+
'{"decision":"complete_support|partial_or_unsupported","reason":"brief evidence-grounded reason"}.'
|
| 4264 |
+
)
|
| 4265 |
+
try:
|
| 4266 |
+
response = self.call_deepseek(
|
| 4267 |
+
system_prompt,
|
| 4268 |
+
json.dumps(payload, ensure_ascii=False),
|
| 4269 |
+
max_tokens=700,
|
| 4270 |
+
temp=0.0,
|
| 4271 |
+
model_role='reasoning',
|
| 4272 |
+
thinking=False,
|
| 4273 |
+
allow_fallback=False,
|
| 4274 |
+
)
|
| 4275 |
+
parsed = self._extract_json_from_text(response)
|
| 4276 |
+
if isinstance(parsed, dict):
|
| 4277 |
+
decision = str(parsed.get('decision') or '').strip().lower()
|
| 4278 |
+
reason = str(parsed.get('reason') or '').strip()
|
| 4279 |
+
if decision == 'complete_support':
|
| 4280 |
+
return True, reason
|
| 4281 |
+
if decision == 'partial_or_unsupported':
|
| 4282 |
+
return False, reason
|
| 4283 |
+
except PipelineCancelledError:
|
| 4284 |
+
raise
|
| 4285 |
+
except Exception as exc:
|
| 4286 |
+
logging.warning(
|
| 4287 |
+
'Citation split adjudication failed closed: section=%s error=%s',
|
| 4288 |
+
section_name, type(exc).__name__,
|
| 4289 |
+
)
|
| 4290 |
+
return False, 'Single-source split adjudication did not confirm complete support.'
|
| 4291 |
+
|
| 4292 |
def semantic_audit_claim_specific_citations(
|
| 4293 |
self,
|
| 4294 |
text,
|
|
|
|
| 4510 |
)
|
| 4511 |
continue
|
| 4512 |
if status == "split":
|
| 4513 |
+
# C5.1: ``split + one tag`` is an inconsistent audit decision,
|
| 4514 |
+
# not proof of complete support. Re-adjudicate the same
|
| 4515 |
+
# sentence/source pair once, without rewrite/retrieval authority.
|
| 4516 |
+
# This preserves C2's legitimate full-support recovery while
|
| 4517 |
+
# preventing a partially unsupported clause from inheriting an
|
| 4518 |
+
# overbroad sentence-final citation.
|
| 4519 |
if len(revised_tags) == 1:
|
| 4520 |
selected = next(iter(revised_tags))
|
| 4521 |
+
complete_support, adjudication_reason = self._adjudicate_single_source_split(
|
| 4522 |
+
sentence=record["original"], selected_tag=selected,
|
| 4523 |
+
evidence=record.get("evidence") or [], split_reason=reason,
|
| 4524 |
+
section_name=section_name,
|
| 4525 |
)
|
| 4526 |
+
if complete_support:
|
| 4527 |
+
replacements[record["id"]] = self._citation_tag_only_rewrite(
|
| 4528 |
+
record["original"], selected
|
| 4529 |
+
)
|
| 4530 |
+
self._append_citation_audit(
|
| 4531 |
+
section_name, "normalize_inconsistent_split_to_single_source",
|
| 4532 |
+
record["original"], replacements[record["id"]],
|
| 4533 |
+
adjudication_reason or reason or "Decision-only adjudication confirmed complete support.",
|
| 4534 |
+
)
|
| 4535 |
+
continue
|
| 4536 |
+
reason = adjudication_reason or reason
|
| 4537 |
violation = {
|
| 4538 |
"type":"citation_claim_split_required", "id":record["id"],
|
| 4539 |
"sentence":record["original"][:500], "reason":reason,
|