Spaces:
Running
Running
S0-R1-C5.5 Pre-Production Contract Freeze Failure-Surface Closure
#83
by GuXiaopeng - opened
- core/generation_runner.py +240 -94
- core/pipeline_agent.py +34 -1
- core/production_guard.py +41 -0
- core/quantitative_engine.py +93 -31
core/generation_runner.py
CHANGED
|
@@ -49,6 +49,28 @@ TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
|
|
| 49 |
FINAL_CITATION_LEDGER_VERSION = 'resolved-final-ledger-v1'
|
| 50 |
ENGLISH_SECTION_SEAL_VERSION = 'english-section-integrity-seal-v1'
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
| 54 |
lines = str(text or "").split("\n")
|
|
@@ -966,6 +988,107 @@ class GenerationRunner:
|
|
| 966 |
rows.append(' | '.join(parts))
|
| 967 |
return '\n'.join(f'- {row}' for row in rows)
|
| 968 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
@staticmethod
|
| 970 |
def _citation_violation_tags(rows) -> set:
|
| 971 |
"""Extract only citation tags owned by the rejected claim sentence.
|
|
@@ -986,24 +1109,10 @@ class GenerationRunner:
|
|
| 986 |
|
| 987 |
@staticmethod
|
| 988 |
def _author_facing_internal_workflow_violations(text: str) -> list[dict]:
|
| 989 |
-
"""Detect
|
| 990 |
-
|
| 991 |
-
The check is intentionally phrase-scoped. A generic word such as
|
| 992 |
-
``frozen`` can be legitimate academic content, but phrases naming the
|
| 993 |
-
private evidence/state machinery are never reader-facing manuscript
|
| 994 |
-
language.
|
| 995 |
-
"""
|
| 996 |
value = str(text or "")
|
| 997 |
-
patterns = [
|
| 998 |
-
r"\bfrozen\s+(?:research\s+)?evidence\b",
|
| 999 |
-
r"\bfrozen\s+evidence\s+packet\b",
|
| 1000 |
-
r"\bevidence\s+packet\b",
|
| 1001 |
-
r"\bfrozen\s+(?:theme\s+summary|thematic\s+structure|statistical\s+evidence|measurement\s+design|association\s+model|bootstrap\s+and\s+collinearity\s+checks|task-level\s+concepts?)\b",
|
| 1002 |
-
r"\b(?:study|result)-owned\s+fact\b",
|
| 1003 |
-
r"\bevidence\s+ledger\b",
|
| 1004 |
-
]
|
| 1005 |
rows = []
|
| 1006 |
-
for pattern in
|
| 1007 |
for match in re.finditer(pattern, value, flags=re.I):
|
| 1008 |
start = max(0, match.start() - 80)
|
| 1009 |
end = min(len(value), match.end() + 120)
|
|
@@ -1011,10 +1120,9 @@ class GenerationRunner:
|
|
| 1011 |
rows.append({
|
| 1012 |
"type": "author_facing_internal_workflow_language",
|
| 1013 |
"preview": preview[:320],
|
| 1014 |
-
"reason": "Private
|
| 1015 |
"repair_instruction": (
|
| 1016 |
-
"Rewrite the sentence as ordinary academic prose describing the study, evidence, method, or limitation directly.
|
| 1017 |
-
"Do not mention frozen evidence, evidence packets, ledgers, study-owned/result-owned facts, or other orchestration labels."
|
| 1018 |
),
|
| 1019 |
})
|
| 1020 |
if len(rows) >= 6:
|
|
@@ -1023,34 +1131,13 @@ class GenerationRunner:
|
|
| 1023 |
|
| 1024 |
@staticmethod
|
| 1025 |
def _writer_visible_evidence_context(text: str) -> str:
|
| 1026 |
-
"""
|
| 1027 |
-
|
| 1028 |
-
Internal evidence/state vocabulary remains unchanged in server-side metadata
|
| 1029 |
-
and validators. This view changes only the control labels presented to the
|
| 1030 |
-
prose model, so AMJ and ASQ receive ordinary scholarly study descriptions
|
| 1031 |
-
instead of phrases that the author-facing purity Gate later has to reject.
|
| 1032 |
-
"""
|
| 1033 |
value = str(text or "")
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
(r"\bfrozen\s+evidence\s+packet\b", "study evidence"),
|
| 1039 |
-
(r"\bevidence\s+packet\b", "study evidence"),
|
| 1040 |
-
(r"\b(?:study\s+)?evidence\s+ledger\b", "study evidence record"),
|
| 1041 |
-
(r"\bfrozen\s+(?:research\s+)?evidence\b", "study evidence"),
|
| 1042 |
-
(r"\bfrozen\s+study\s+evidence\b", "study evidence"),
|
| 1043 |
-
(r"\bfrozen\s+(?:research\s+|study\s+)?(facts?|numbers?|design|setting|sample|measures?|results?|themes?|quotations?|procedures?)\b", r"documented \1"),
|
| 1044 |
-
(r"\b(?:study|result)-owned\s+facts?\b", "study-specific facts"),
|
| 1045 |
-
(r"\bfrozen\s+task-level\s+concepts?\b", "canonical research concepts"),
|
| 1046 |
-
(r"\bfrozen\s+theme\s+summary\b", "theme summary"),
|
| 1047 |
-
(r"\bfrozen\s+thematic\s+structure\b", "thematic structure"),
|
| 1048 |
-
(r"\bfrozen\s+statistical\s+evidence\b", "study statistical evidence"),
|
| 1049 |
-
)
|
| 1050 |
-
for pattern, replacement in substitutions:
|
| 1051 |
value = re.sub(pattern, replacement, value, flags=re.I)
|
| 1052 |
-
# Evidence renderers sometimes append an Executor-only sentence after a
|
| 1053 |
-
# reader-relevant instruction. Drop only that private suffix.
|
| 1054 |
value = re.sub(r"(?im);\s*the Executor\b[^\n]*", ".", value)
|
| 1055 |
value = re.sub(r"(?im)^\s*the Executor\b[^\n]*$", "", value)
|
| 1056 |
return value
|
|
@@ -2392,6 +2479,7 @@ class GenerationRunner:
|
|
| 2392 |
else:
|
| 2393 |
state = self.checkpoints.load_confirmed() or {}
|
| 2394 |
quality_diagnostics = list(state.get("quality_diagnostics") or [])
|
|
|
|
| 2395 |
self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
|
| 2396 |
if str(state.get("evidence_status") or "") == "limited":
|
| 2397 |
self._preview_evidence_status = "limited"
|
|
@@ -3355,6 +3443,13 @@ class GenerationRunner:
|
|
| 3355 |
"\nCURRENT-STUDY PROVENANCE RULE: Do not attach external literature citations to this study's own research question, H1-Hn statement, sample/design fact, analytic choice, result, limitation specific to the available study evidence, or author positioning. "
|
| 3356 |
"If prior literature provides a rationale, state that external rationale in a separate sentence and cite only that sentence."
|
| 3357 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3358 |
if bool(state.get("evidence_risk_detected")) and section_id not in {"results", "findings", "appendix"}:
|
| 3359 |
citation_rule += (
|
| 3360 |
"\nEVIDENCE-LIMITED CLAIM-DENSITY CONTRACT: Direct high-relevance literature coverage is limited. Treat assigned claim-evidence items as the ceiling for source-dependent empirical, definitional, prevalence, and field-gap claims in this unit. "
|
|
@@ -3465,6 +3560,15 @@ class GenerationRunner:
|
|
| 3465 |
prompt_context_tail = context_tail[-2500:]
|
| 3466 |
if mode == "asq":
|
| 3467 |
prompt_context_tail = self._redact_qualitative_quotes(prompt_context_tail, data_metadata)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3468 |
user_prompt = f"""Write the assigned English manuscript unit.
|
| 3469 |
Section: {sec_title}
|
| 3470 |
Subsection: {subsection or 'None'}
|
|
@@ -3489,7 +3593,7 @@ AUTHOR-FACING PURITY: Write only reader-facing scholarly prose. Do not mention i
|
|
| 3489 |
</WORKING_ABSTRACT_ANCHOR>
|
| 3490 |
ANCHOR PRECEDENCE: The STUDY_EVIDENCE and PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
|
| 3491 |
{memory_block}
|
| 3492 |
-
Literature synthesis: {
|
| 3493 |
<STUDY_EVIDENCE>
|
| 3494 |
{writer_scoped_data_md}
|
| 3495 |
</STUDY_EVIDENCE>
|
|
@@ -3601,6 +3705,11 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3601 |
if mode == "asq":
|
| 3602 |
# Model prose has no authority over quote presence/count.
|
| 3603 |
en_raw = self._strip_model_controlled_qualitative_quotes(en_raw, data_metadata)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3604 |
|
| 3605 |
# S0-R1-C4 phase-1 barrier: all writer-owned semantic validators
|
| 3606 |
# inspect the same attempt before the single same-Section retry budget
|
|
@@ -3621,8 +3730,11 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3621 |
set(attempt_assigned_tag_list) - set(retry_excluded_tags)
|
| 3622 |
if not is_quarantined else set()
|
| 3623 |
)
|
|
|
|
|
|
|
|
|
|
| 3624 |
en_resolved = agent.audit_and_resolve_citations(
|
| 3625 |
-
|
| 3626 |
quarantine=is_quarantined, allowed_tags=allowed_section_citation_tags
|
| 3627 |
)
|
| 3628 |
# Capture immediately: later recovery may replace the agent's last-
|
|
@@ -3632,51 +3744,67 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3632 |
list(getattr(agent, "_last_citation_semantic_violations", []) or [])
|
| 3633 |
)
|
| 3634 |
phase1_validation_rows = self._section_validation_bundle(
|
| 3635 |
-
mode, agent, quant_engine,
|
| 3636 |
section_id=section_id, section_name=display_name,
|
| 3637 |
evidence_contract=evidence_packet,
|
| 3638 |
include_critic=not deterministic_appendix,
|
| 3639 |
citation_violations=citation_semantic_rows, hypothesis_ledger=hypothesis_ledger,
|
| 3640 |
)
|
| 3641 |
-
non_citation_count = max(
|
| 3642 |
-
0, len(phase1_validation_rows) - len(citation_semantic_rows)
|
| 3643 |
-
)
|
| 3644 |
if phase1_validation_rows:
|
| 3645 |
retry_excluded_tags.update(self._citation_violation_tags(citation_semantic_rows))
|
| 3646 |
-
|
| 3647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3648 |
logging.warning(
|
| 3649 |
"Unified Phase-1 Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3650 |
-
display_name, section_attempt, len(
|
| 3651 |
sorted({
|
| 3652 |
str(row.get("type") or "")
|
| 3653 |
-
for row in
|
| 3654 |
}),
|
| 3655 |
)
|
| 3656 |
self._restore_section_citation_state(
|
| 3657 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3658 |
)
|
| 3659 |
continue
|
| 3660 |
-
|
| 3661 |
-
|
| 3662 |
-
|
| 3663 |
-
|
| 3664 |
-
|
| 3665 |
-
|
| 3666 |
-
|
| 3667 |
-
|
| 3668 |
-
|
| 3669 |
-
|
| 3670 |
-
|
| 3671 |
-
|
| 3672 |
-
|
| 3673 |
-
|
| 3674 |
-
|
| 3675 |
-
|
| 3676 |
-
|
| 3677 |
-
|
| 3678 |
-
|
| 3679 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3680 |
|
| 3681 |
used_after = set(agent.used_citations)
|
| 3682 |
survived = used_after - used_before
|
|
@@ -3802,32 +3930,47 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3802 |
hypothesis_ledger=hypothesis_ledger,
|
| 3803 |
)
|
| 3804 |
if post_validation_rows:
|
| 3805 |
-
|
| 3806 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3807 |
logging.warning(
|
| 3808 |
"Post-normalization Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3809 |
-
display_name, section_attempt, len(
|
| 3810 |
-
sorted({str(row.get("type") or "") for row in
|
| 3811 |
)
|
| 3812 |
self._restore_section_citation_state(
|
| 3813 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3814 |
)
|
| 3815 |
continue
|
| 3816 |
-
|
| 3817 |
-
|
| 3818 |
-
|
| 3819 |
-
|
| 3820 |
-
|
| 3821 |
-
|
| 3822 |
-
|
| 3823 |
-
|
| 3824 |
-
|
| 3825 |
-
|
| 3826 |
-
|
| 3827 |
-
|
| 3828 |
-
|
| 3829 |
-
|
| 3830 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3831 |
|
| 3832 |
quality_diagnostics.extend(attempt_diagnostics)
|
| 3833 |
assigned_tag_list = list(attempt_assigned_tag_list)
|
|
@@ -3942,6 +4085,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3942 |
"context_tail": context_tail,
|
| 3943 |
"accumulated_deviation": accumulated_deviation,
|
| 3944 |
"quality_diagnostics": quality_diagnostics,
|
|
|
|
| 3945 |
"citation_plan": citation_plan,
|
| 3946 |
"citation_minimums": citation_minimums,
|
| 3947 |
"citation_reserve_targets": citation_reserve_targets,
|
|
@@ -4388,6 +4532,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 4388 |
citation_audit=agent.citation_audit_log,
|
| 4389 |
literature_metrics=agent.get_source_metrics(),
|
| 4390 |
quality_diagnostics=quality_diagnostics,
|
|
|
|
| 4391 |
evidence_coverage=state.get("evidence_coverage_summary") or {
|
| 4392 |
"status": str(state.get("evidence_status") or "adequate"),
|
| 4393 |
"early_risk_detected": bool(state.get("evidence_risk_detected")),
|
|
@@ -4408,6 +4553,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 4408 |
"package_validated": True,
|
| 4409 |
"evidence_status": str(state.get("evidence_status") or "adequate"),
|
| 4410 |
"evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
|
|
|
|
| 4411 |
"agent_state": agent.export_state(),
|
| 4412 |
}
|
| 4413 |
)
|
|
|
|
| 49 |
FINAL_CITATION_LEDGER_VERSION = 'resolved-final-ledger-v1'
|
| 50 |
ENGLISH_SECTION_SEAL_VERSION = 'english-section-integrity-seal-v1'
|
| 51 |
|
| 52 |
+
# S0-R1-C5.5: one shared private-vocabulary registry is consumed by both
|
| 53 |
+
# Writer-facing sanitization and final reader-facing validation. Keeping a
|
| 54 |
+
# single registry prevents the historical drift where a phrase was allowed
|
| 55 |
+
# into the Writer prompt/output but rejected later by the purity Gate.
|
| 56 |
+
AUTHOR_FACING_PRIVATE_VOCABULARY = (
|
| 57 |
+
(r"\bfrozen\s+research\s+evidence\s+packet\b", "study evidence"),
|
| 58 |
+
(r"\bfrozen\s+evidence\s+packet\b", "study evidence"),
|
| 59 |
+
(r"\bevidence\s+packet\b", "study evidence"),
|
| 60 |
+
(r"\b(?:study\s+)?evidence\s+ledger\b", "study evidence record"),
|
| 61 |
+
(r"\bfrozen\s+(?:research\s+)?evidence\b", "study evidence"),
|
| 62 |
+
(r"\bfrozen\s+study\s+evidence\b", "study evidence"),
|
| 63 |
+
(r"\bfrozen\s+(?:research\s+|study\s+)?(facts?|numbers?|design|setting|sample|measures?|results?|themes?|quotations?|procedures?)\b", r"documented \1"),
|
| 64 |
+
(r"\b(?:study|result)-owned\s+facts?\b", "study-specific facts"),
|
| 65 |
+
(r"\bfrozen\s+task-level\s+concepts?\b", "canonical research concepts"),
|
| 66 |
+
(r"\bfrozen\s+theme\s+summary\b", "theme summary"),
|
| 67 |
+
(r"\bfrozen\s+thematic\s+structure\b", "thematic structure"),
|
| 68 |
+
(r"\bfrozen\s+statistical\s+evidence\b", "study statistical evidence"),
|
| 69 |
+
(r"\bfrozen\s+measurement\s+design\b", "documented measurement design"),
|
| 70 |
+
(r"\bfrozen\s+association\s+model\b", "specified association model"),
|
| 71 |
+
(r"\bfrozen\s+bootstrap\s+and\s+collinearity\s+checks\b", "documented bootstrap and collinearity checks"),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
|
| 75 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
| 76 |
lines = str(text or "").split("\n")
|
|
|
|
| 988 |
rows.append(' | '.join(parts))
|
| 989 |
return '\n'.join(f'- {row}' for row in rows)
|
| 990 |
|
| 991 |
+
@staticmethod
|
| 992 |
+
def _section_violation_severity(row, *, section_name: str = "", mode: str = "", exhausted: bool = False) -> str:
|
| 993 |
+
"""Map one validation issue to REPAIRABLE/FATAL/ADVISORY.
|
| 994 |
+
|
| 995 |
+
Before the one local retry, all substantive correctness issues remain
|
| 996 |
+
repairable. After retry exhaustion only issues that can change study
|
| 997 |
+
facts, core inference, hypothesis identity, core evidence support, or
|
| 998 |
+
citation integrity may terminate the manuscript. Minor reader-facing
|
| 999 |
+
style/background defects become advisory notes instead of destroying an
|
| 1000 |
+
otherwise complete paper.
|
| 1001 |
+
"""
|
| 1002 |
+
item = dict(row or {}) if isinstance(row, dict) else {"type": "unknown_validation_issue"}
|
| 1003 |
+
kind = str(item.get("type") or "unknown_validation_issue")
|
| 1004 |
+
section = str(section_name or "")
|
| 1005 |
+
if not exhausted:
|
| 1006 |
+
if kind == "top_journal_critic":
|
| 1007 |
+
return "ADVISORY"
|
| 1008 |
+
return "REPAIRABLE"
|
| 1009 |
+
|
| 1010 |
+
always_fatal = {
|
| 1011 |
+
"causal_language_violation",
|
| 1012 |
+
"sample_size_mismatch", "sample_count_mismatch", "scale_points_mismatch",
|
| 1013 |
+
"construct_role_mismatch", "quantitative_metric_mismatch", "unfrozen_p_value",
|
| 1014 |
+
"unsupported_quantitative_method", "actor_data_source_relation_mismatch",
|
| 1015 |
+
"unsupported_study_procedure", "enumeration_contract_invalid",
|
| 1016 |
+
"hypothesis_contract_missing_id", "hypothesis_contract_ordinal_alias",
|
| 1017 |
+
"hypothesis_contract_pairing_mismatch", "hypothesis_contract_unknown_id",
|
| 1018 |
+
"citation_outside_frozen_section_whitelist", "authorial_ai_meta_trace",
|
| 1019 |
+
}
|
| 1020 |
+
if kind in always_fatal:
|
| 1021 |
+
return "FATAL"
|
| 1022 |
+
|
| 1023 |
+
if kind in {"author_facing_internal_workflow_language", "top_journal_critic"}:
|
| 1024 |
+
return "ADVISORY"
|
| 1025 |
+
|
| 1026 |
+
if kind in {"citation_claim_split_required", "citation_unsupported_claim", "citation_support_unverified"}:
|
| 1027 |
+
core_citation_section = bool(re.search(
|
| 1028 |
+
r"(?i)(?:Theoretical Mechanisms|Hypothesis Development|Methodology|Results?|Findings?)",
|
| 1029 |
+
section,
|
| 1030 |
+
))
|
| 1031 |
+
return "FATAL" if core_citation_section else "ADVISORY"
|
| 1032 |
+
|
| 1033 |
+
# Unknown validation types remain fail-closed. This keeps C5.5 from
|
| 1034 |
+
# silently downgrading a future integrity contract merely because it is
|
| 1035 |
+
# new to the severity registry.
|
| 1036 |
+
return "FATAL"
|
| 1037 |
+
|
| 1038 |
+
@classmethod
|
| 1039 |
+
def _partition_section_violations(cls, rows, *, section_name: str = "", mode: str = "", exhausted: bool = False):
|
| 1040 |
+
blocking, advisory = [], []
|
| 1041 |
+
for row in list(rows or []):
|
| 1042 |
+
severity = cls._section_violation_severity(
|
| 1043 |
+
row, section_name=section_name, mode=mode, exhausted=exhausted
|
| 1044 |
+
)
|
| 1045 |
+
if severity == "ADVISORY":
|
| 1046 |
+
advisory.append(row)
|
| 1047 |
+
else:
|
| 1048 |
+
blocking.append(row)
|
| 1049 |
+
return blocking, advisory
|
| 1050 |
+
|
| 1051 |
+
@staticmethod
|
| 1052 |
+
def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None:
|
| 1053 |
+
"""Append reader-facing, nonfatal notes without exposing control vocabulary."""
|
| 1054 |
+
for row in list(rows or []):
|
| 1055 |
+
if not isinstance(row, dict):
|
| 1056 |
+
continue
|
| 1057 |
+
kind = str(row.get("type") or "quality_advisory")
|
| 1058 |
+
if kind == "citation_claim_split_required":
|
| 1059 |
+
detail = "A non-core literature sentence may combine more than one externally verifiable proposition."
|
| 1060 |
+
action = "Before formal submission, consider splitting that sentence and checking the citation attached to each proposition."
|
| 1061 |
+
elif kind in {"citation_unsupported_claim", "citation_support_unverified"}:
|
| 1062 |
+
detail = "A non-core literature statement could not be fully verified against the available retrieved evidence."
|
| 1063 |
+
action = "Review the statement and its supporting literature before formal submission; the manuscript was completed because the issue does not alter the study's core results or design."
|
| 1064 |
+
elif kind == "author_facing_internal_workflow_language":
|
| 1065 |
+
detail = "A minor reader-facing wording issue remained after automatic revision."
|
| 1066 |
+
action = "Review the indicated passage for ordinary academic phrasing before formal submission."
|
| 1067 |
+
elif kind == "authorial_ai_meta_trace":
|
| 1068 |
+
detail = "A minor author-facing wording pattern may benefit from manual stylistic review."
|
| 1069 |
+
action = "Review the indicated passage and remove any wording that does not read as ordinary scholarly prose."
|
| 1070 |
+
elif kind == "top_journal_critic":
|
| 1071 |
+
detail = str(row.get("reason") or "An optional journal-style refinement was identified.")[:500]
|
| 1072 |
+
action = "This is a non-blocking style suggestion; revise only if it improves conciseness, accuracy, or rigor."
|
| 1073 |
+
else:
|
| 1074 |
+
detail = "A nonfatal quality issue was recorded for author review."
|
| 1075 |
+
action = "Review the indicated section before formal submission if additional polishing is desired."
|
| 1076 |
+
note = {
|
| 1077 |
+
"type": "section_quality_advisory",
|
| 1078 |
+
"severity": "ADVISORY",
|
| 1079 |
+
"section": str(section_name or "Manuscript"),
|
| 1080 |
+
"issue_type": kind,
|
| 1081 |
+
"detail": detail,
|
| 1082 |
+
"user_action": action,
|
| 1083 |
+
}
|
| 1084 |
+
signature = (note["section"], note["issue_type"], note["detail"])
|
| 1085 |
+
if any(
|
| 1086 |
+
(str(existing.get("section") or ""), str(existing.get("issue_type") or ""), str(existing.get("detail") or "")) == signature
|
| 1087 |
+
for existing in quality_notes if isinstance(existing, dict)
|
| 1088 |
+
):
|
| 1089 |
+
continue
|
| 1090 |
+
quality_notes.append(note)
|
| 1091 |
+
|
| 1092 |
@staticmethod
|
| 1093 |
def _citation_violation_tags(rows) -> set:
|
| 1094 |
"""Extract only citation tags owned by the rejected claim sentence.
|
|
|
|
| 1109 |
|
| 1110 |
@staticmethod
|
| 1111 |
def _author_facing_internal_workflow_violations(text: str) -> list[dict]:
|
| 1112 |
+
"""Detect reader-facing leakage using the same registry as sanitization."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
value = str(text or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
rows = []
|
| 1115 |
+
for pattern, _replacement in AUTHOR_FACING_PRIVATE_VOCABULARY:
|
| 1116 |
for match in re.finditer(pattern, value, flags=re.I):
|
| 1117 |
start = max(0, match.start() - 80)
|
| 1118 |
end = min(len(value), match.end() + 120)
|
|
|
|
| 1120 |
rows.append({
|
| 1121 |
"type": "author_facing_internal_workflow_language",
|
| 1122 |
"preview": preview[:320],
|
| 1123 |
+
"reason": "Private generation-control vocabulary surfaced in reader-facing manuscript prose.",
|
| 1124 |
"repair_instruction": (
|
| 1125 |
+
"Rewrite the sentence as ordinary academic prose describing the study, evidence, method, or limitation directly."
|
|
|
|
| 1126 |
),
|
| 1127 |
})
|
| 1128 |
if len(rows) >= 6:
|
|
|
|
| 1131 |
|
| 1132 |
@staticmethod
|
| 1133 |
def _writer_visible_evidence_context(text: str) -> str:
|
| 1134 |
+
"""Return a reader-safe Writer view using the shared private registry."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
value = str(text or "")
|
| 1136 |
+
# Formatting-only labels are sanitized before the phrase registry.
|
| 1137 |
+
value = re.sub(r"(?im)^(#{1,6}\s+)Frozen\s+", r"\1", value)
|
| 1138 |
+
value = re.sub(r"(?im)^\|\s*Field\s*\|\s*Frozen value\s*\|$", "| Field | Study value |", value)
|
| 1139 |
+
for pattern, replacement in AUTHOR_FACING_PRIVATE_VOCABULARY:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
value = re.sub(pattern, replacement, value, flags=re.I)
|
|
|
|
|
|
|
| 1141 |
value = re.sub(r"(?im);\s*the Executor\b[^\n]*", ".", value)
|
| 1142 |
value = re.sub(r"(?im)^\s*the Executor\b[^\n]*$", "", value)
|
| 1143 |
return value
|
|
|
|
| 2479 |
else:
|
| 2480 |
state = self.checkpoints.load_confirmed() or {}
|
| 2481 |
quality_diagnostics = list(state.get("quality_diagnostics") or [])
|
| 2482 |
+
quality_notes = list(state.get("quality_notes") or [])
|
| 2483 |
self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
|
| 2484 |
if str(state.get("evidence_status") or "") == "limited":
|
| 2485 |
self._preview_evidence_status = "limited"
|
|
|
|
| 3443 |
"\nCURRENT-STUDY PROVENANCE RULE: Do not attach external literature citations to this study's own research question, H1-Hn statement, sample/design fact, analytic choice, result, limitation specific to the available study evidence, or author positioning. "
|
| 3444 |
"If prior literature provides a rationale, state that external rationale in a separate sentence and cite only that sentence."
|
| 3445 |
)
|
| 3446 |
+
if section_id not in {"results", "findings", "appendix"}:
|
| 3447 |
+
citation_rule += (
|
| 3448 |
+
"\nATOMIC LITERATURE CLAIM CONTRACT: Write one externally verifiable proposition per sentence. "
|
| 3449 |
+
"Do not combine two literature-dependent findings, mechanisms, definitions, or outcome claims under one citation. "
|
| 3450 |
+
"When two propositions require different sources, write two sentences and attach each sentence only to its own assigned source. "
|
| 3451 |
+
"A current-study fact or H1-Hn/RQ statement must remain citation-free unless a separate prior-literature rationale is stated in its own sentence."
|
| 3452 |
+
)
|
| 3453 |
if bool(state.get("evidence_risk_detected")) and section_id not in {"results", "findings", "appendix"}:
|
| 3454 |
citation_rule += (
|
| 3455 |
"\nEVIDENCE-LIMITED CLAIM-DENSITY CONTRACT: Direct high-relevance literature coverage is limited. Treat assigned claim-evidence items as the ceiling for source-dependent empirical, definitional, prevalence, and field-gap claims in this unit. "
|
|
|
|
| 3560 |
prompt_context_tail = context_tail[-2500:]
|
| 3561 |
if mode == "asq":
|
| 3562 |
prompt_context_tail = self._redact_qualitative_quotes(prompt_context_tail, data_metadata)
|
| 3563 |
+
if bool(state.get("evidence_risk_detected")):
|
| 3564 |
+
writer_synthesis_context = (
|
| 3565 |
+
"EVIDENCE-LIMITED SYNTHESIS BOUNDARY: direct claim-specific literature coverage is limited. "
|
| 3566 |
+
"The claim-evidence plan is the only source-dependent manuscript authority for externally verifiable literature statements in this unit. "
|
| 3567 |
+
"Do not convert broad synthesis themes, general model knowledge, or unassigned references into manuscript claims. "
|
| 3568 |
+
"When no supported external claim is assigned, prefer concise study positioning, transparent limitations, and cautious conceptual reasoning without invented citation support."
|
| 3569 |
+
)
|
| 3570 |
+
else:
|
| 3571 |
+
writer_synthesis_context = synthesis_result
|
| 3572 |
user_prompt = f"""Write the assigned English manuscript unit.
|
| 3573 |
Section: {sec_title}
|
| 3574 |
Subsection: {subsection or 'None'}
|
|
|
|
| 3593 |
</WORKING_ABSTRACT_ANCHOR>
|
| 3594 |
ANCHOR PRECEDENCE: The STUDY_EVIDENCE and PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
|
| 3595 |
{memory_block}
|
| 3596 |
+
Literature synthesis: {writer_synthesis_context}
|
| 3597 |
<STUDY_EVIDENCE>
|
| 3598 |
{writer_scoped_data_md}
|
| 3599 |
</STUDY_EVIDENCE>
|
|
|
|
| 3705 |
if mode == "asq":
|
| 3706 |
# Model prose has no authority over quote presence/count.
|
| 3707 |
en_raw = self._strip_model_controlled_qualitative_quotes(en_raw, data_metadata)
|
| 3708 |
+
# C5.5 uses the same private-vocabulary registry for both
|
| 3709 |
+
# Writer-facing evidence and reader-facing output. This
|
| 3710 |
+
# deterministic phrasing cleanup cannot change study facts,
|
| 3711 |
+
# citations, hypotheses, numbers, or quote ownership.
|
| 3712 |
+
en_raw = self._writer_visible_evidence_context(en_raw)
|
| 3713 |
|
| 3714 |
# S0-R1-C4 phase-1 barrier: all writer-owned semantic validators
|
| 3715 |
# inspect the same attempt before the single same-Section retry budget
|
|
|
|
| 3730 |
set(attempt_assigned_tag_list) - set(retry_excluded_tags)
|
| 3731 |
if not is_quarantined else set()
|
| 3732 |
)
|
| 3733 |
+
attempt_section_text = agent.strip_ineligible_current_study_citations(
|
| 3734 |
+
prefix + en_raw, display_name
|
| 3735 |
+
)
|
| 3736 |
en_resolved = agent.audit_and_resolve_citations(
|
| 3737 |
+
attempt_section_text, section_name=display_name, section_id=section_id,
|
| 3738 |
quarantine=is_quarantined, allowed_tags=allowed_section_citation_tags
|
| 3739 |
)
|
| 3740 |
# Capture immediately: later recovery may replace the agent's last-
|
|
|
|
| 3744 |
list(getattr(agent, "_last_citation_semantic_violations", []) or [])
|
| 3745 |
)
|
| 3746 |
phase1_validation_rows = self._section_validation_bundle(
|
| 3747 |
+
mode, agent, quant_engine, attempt_section_text, data_metadata,
|
| 3748 |
section_id=section_id, section_name=display_name,
|
| 3749 |
evidence_contract=evidence_packet,
|
| 3750 |
include_critic=not deterministic_appendix,
|
| 3751 |
citation_violations=citation_semantic_rows, hypothesis_ledger=hypothesis_ledger,
|
| 3752 |
)
|
|
|
|
|
|
|
|
|
|
| 3753 |
if phase1_validation_rows:
|
| 3754 |
retry_excluded_tags.update(self._citation_violation_tags(citation_semantic_rows))
|
| 3755 |
+
exhausted = section_attempt >= max_section_attempts
|
| 3756 |
+
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3757 |
+
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3758 |
+
)
|
| 3759 |
+
if blocking_rows and not exhausted:
|
| 3760 |
+
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 3761 |
logging.warning(
|
| 3762 |
"Unified Phase-1 Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3763 |
+
display_name, section_attempt, len(blocking_rows),
|
| 3764 |
sorted({
|
| 3765 |
str(row.get("type") or "")
|
| 3766 |
+
for row in blocking_rows if isinstance(row, dict)
|
| 3767 |
}),
|
| 3768 |
)
|
| 3769 |
self._restore_section_citation_state(
|
| 3770 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3771 |
)
|
| 3772 |
continue
|
| 3773 |
+
if advisory_rows:
|
| 3774 |
+
self._record_section_advisories(
|
| 3775 |
+
quality_notes, advisory_rows, section_name=display_name
|
| 3776 |
+
)
|
| 3777 |
+
logging.warning(
|
| 3778 |
+
"Nonfatal section quality advisory | unit=%s count=%s types=%s",
|
| 3779 |
+
display_name, len(advisory_rows),
|
| 3780 |
+
sorted({str(row.get("type") or "") for row in advisory_rows if isinstance(row, dict)}),
|
| 3781 |
+
)
|
| 3782 |
+
if blocking_rows:
|
| 3783 |
+
self._restore_section_citation_state(
|
| 3784 |
+
agent, citation_plan, display_name, frozen_citation_state
|
| 3785 |
+
)
|
| 3786 |
+
state.update({
|
| 3787 |
+
"citation_plan": citation_plan,
|
| 3788 |
+
"agent_state": agent.export_state(),
|
| 3789 |
+
"quality_diagnostics": quality_diagnostics,
|
| 3790 |
+
"quality_notes": quality_notes,
|
| 3791 |
+
})
|
| 3792 |
+
self._checkpoint_section_retry_exhausted(
|
| 3793 |
+
state, section_id=section_id, section_name=display_name,
|
| 3794 |
+
violations=blocking_rows, pct=pct, attempts=max_section_attempts,
|
| 3795 |
+
)
|
| 3796 |
+
blocking_citation_rows = [
|
| 3797 |
+
row for row in blocking_rows
|
| 3798 |
+
if isinstance(row, dict) and str(row.get("type") or "").startswith("citation_")
|
| 3799 |
+
]
|
| 3800 |
+
terminal_kind = (
|
| 3801 |
+
"section_citation_invalid_after_retry"
|
| 3802 |
+
if blocking_citation_rows and len(blocking_citation_rows) == len(blocking_rows)
|
| 3803 |
+
else "section_content_invalid_after_retry"
|
| 3804 |
+
)
|
| 3805 |
+
raise RuntimeError(
|
| 3806 |
+
f"{terminal_kind}:{display_name}:{blocking_rows[:3]}"
|
| 3807 |
+
)
|
| 3808 |
|
| 3809 |
used_after = set(agent.used_citations)
|
| 3810 |
survived = used_after - used_before
|
|
|
|
| 3930 |
hypothesis_ledger=hypothesis_ledger,
|
| 3931 |
)
|
| 3932 |
if post_validation_rows:
|
| 3933 |
+
exhausted = section_attempt >= max_section_attempts
|
| 3934 |
+
blocking_rows, advisory_rows = self._partition_section_violations(
|
| 3935 |
+
post_validation_rows, section_name=display_name, mode=mode, exhausted=exhausted
|
| 3936 |
+
)
|
| 3937 |
+
if blocking_rows and not exhausted:
|
| 3938 |
+
attempt_feedback = self._section_retry_feedback(blocking_rows)
|
| 3939 |
logging.warning(
|
| 3940 |
"Post-normalization Section retry | unit=%s attempt=%s count=%s types=%s",
|
| 3941 |
+
display_name, section_attempt, len(blocking_rows),
|
| 3942 |
+
sorted({str(row.get("type") or "") for row in blocking_rows if isinstance(row, dict)}),
|
| 3943 |
)
|
| 3944 |
self._restore_section_citation_state(
|
| 3945 |
agent, citation_plan, display_name, frozen_citation_state
|
| 3946 |
)
|
| 3947 |
continue
|
| 3948 |
+
if advisory_rows:
|
| 3949 |
+
self._record_section_advisories(
|
| 3950 |
+
quality_notes, advisory_rows, section_name=display_name
|
| 3951 |
+
)
|
| 3952 |
+
logging.warning(
|
| 3953 |
+
"Nonfatal post-normalization quality advisory | unit=%s count=%s types=%s",
|
| 3954 |
+
display_name, len(advisory_rows),
|
| 3955 |
+
sorted({str(row.get("type") or "") for row in advisory_rows if isinstance(row, dict)}),
|
| 3956 |
+
)
|
| 3957 |
+
if blocking_rows:
|
| 3958 |
+
self._restore_section_citation_state(
|
| 3959 |
+
agent, citation_plan, display_name, frozen_citation_state
|
| 3960 |
+
)
|
| 3961 |
+
state.update({
|
| 3962 |
+
"citation_plan": citation_plan,
|
| 3963 |
+
"agent_state": agent.export_state(),
|
| 3964 |
+
"quality_diagnostics": quality_diagnostics,
|
| 3965 |
+
"quality_notes": quality_notes,
|
| 3966 |
+
})
|
| 3967 |
+
self._checkpoint_section_retry_exhausted(
|
| 3968 |
+
state, section_id=section_id, section_name=display_name,
|
| 3969 |
+
violations=blocking_rows, pct=pct, attempts=max_section_attempts,
|
| 3970 |
+
)
|
| 3971 |
+
raise RuntimeError(
|
| 3972 |
+
f"section_content_invalid_after_retry:{display_name}:{blocking_rows[:3]}"
|
| 3973 |
+
)
|
| 3974 |
|
| 3975 |
quality_diagnostics.extend(attempt_diagnostics)
|
| 3976 |
assigned_tag_list = list(attempt_assigned_tag_list)
|
|
|
|
| 4085 |
"context_tail": context_tail,
|
| 4086 |
"accumulated_deviation": accumulated_deviation,
|
| 4087 |
"quality_diagnostics": quality_diagnostics,
|
| 4088 |
+
"quality_notes": quality_notes,
|
| 4089 |
"citation_plan": citation_plan,
|
| 4090 |
"citation_minimums": citation_minimums,
|
| 4091 |
"citation_reserve_targets": citation_reserve_targets,
|
|
|
|
| 4532 |
citation_audit=agent.citation_audit_log,
|
| 4533 |
literature_metrics=agent.get_source_metrics(),
|
| 4534 |
quality_diagnostics=quality_diagnostics,
|
| 4535 |
+
quality_notes=quality_notes,
|
| 4536 |
evidence_coverage=state.get("evidence_coverage_summary") or {
|
| 4537 |
"status": str(state.get("evidence_status") or "adequate"),
|
| 4538 |
"early_risk_detected": bool(state.get("evidence_risk_detected")),
|
|
|
|
| 4553 |
"package_validated": True,
|
| 4554 |
"evidence_status": str(state.get("evidence_status") or "adequate"),
|
| 4555 |
"evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
|
| 4556 |
+
"quality_notes": quality_notes,
|
| 4557 |
"agent_state": agent.export_state(),
|
| 4558 |
}
|
| 4559 |
)
|
core/pipeline_agent.py
CHANGED
|
@@ -3304,6 +3304,39 @@ INPUT:
|
|
| 3304 |
def _citation_recovery_eligible_claim(cls, sentence, section_name=''):
|
| 3305 |
return cls._citation_claim_provenance(sentence, section_name) == 'EXTERNAL_LITERATURE_CLAIM'
|
| 3306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3307 |
@classmethod
|
| 3308 |
def count_external_literature_claims(cls, text, section_name=''):
|
| 3309 |
return sum(
|
|
@@ -4883,7 +4916,7 @@ Return exactly:
|
|
| 4883 |
self, text, section_name='', section_id='', quarantine=False, allowed_tags=None
|
| 4884 |
):
|
| 4885 |
self._last_citation_semantic_violations = []
|
| 4886 |
-
value = str(text or '')
|
| 4887 |
if quarantine:
|
| 4888 |
return self.resolve_citations(value, quarantine=True)
|
| 4889 |
unknown = sorted({f"[{tag.upper()}]" for tag in re.findall(r'CITE-\d+', value, re.I)} - set(self.citation_map))
|
|
|
|
| 3304 |
def _citation_recovery_eligible_claim(cls, sentence, section_name=''):
|
| 3305 |
return cls._citation_claim_provenance(sentence, section_name) == 'EXTERNAL_LITERATURE_CLAIM'
|
| 3306 |
|
| 3307 |
+
def strip_ineligible_current_study_citations(self, text, section_name=''):
|
| 3308 |
+
"""Remove external citation tags from current-study/provenance-owned prose.
|
| 3309 |
+
|
| 3310 |
+
C5.5 moves provenance enforcement in front of semantic citation audit.
|
| 3311 |
+
This is TAG-ONLY mutation: study facts, H1-Hn statements, research
|
| 3312 |
+
questions, result-owned claims, and structural sentences keep their
|
| 3313 |
+
prose unchanged while any model-added [CITE-XX] tag is removed.
|
| 3314 |
+
"""
|
| 3315 |
+
value = str(text or '')
|
| 3316 |
+
rebuilt = []
|
| 3317 |
+
cursor = 0
|
| 3318 |
+
for start, end, sentence in self._sentence_spans(value):
|
| 3319 |
+
rebuilt.append(value[cursor:start])
|
| 3320 |
+
original = str(sentence or '')
|
| 3321 |
+
if re.search(r'CITE-\d+', original, re.I):
|
| 3322 |
+
provenance = self._citation_claim_provenance(original, section_name)
|
| 3323 |
+
if provenance != 'EXTERNAL_LITERATURE_CLAIM':
|
| 3324 |
+
revised = self._citation_tag_only_rewrite(original, None)
|
| 3325 |
+
self._append_citation_audit(
|
| 3326 |
+
section_name,
|
| 3327 |
+
'remove_nonliterature_citation_by_provenance',
|
| 3328 |
+
original, revised, provenance,
|
| 3329 |
+
)
|
| 3330 |
+
rebuilt.append(revised)
|
| 3331 |
+
cursor = end
|
| 3332 |
+
continue
|
| 3333 |
+
rebuilt.append(original)
|
| 3334 |
+
cursor = end
|
| 3335 |
+
rebuilt.append(value[cursor:])
|
| 3336 |
+
cleaned = ''.join(rebuilt)
|
| 3337 |
+
cleaned = re.sub(r'[ \t]+\n', '\n', cleaned)
|
| 3338 |
+
return re.sub(r'\n{3,}', '\n\n', cleaned).strip()
|
| 3339 |
+
|
| 3340 |
@classmethod
|
| 3341 |
def count_external_literature_claims(cls, text, section_name=''):
|
| 3342 |
return sum(
|
|
|
|
| 4916 |
self, text, section_name='', section_id='', quarantine=False, allowed_tags=None
|
| 4917 |
):
|
| 4918 |
self._last_citation_semantic_violations = []
|
| 4919 |
+
value = self.strip_ineligible_current_study_citations(str(text or ''), section_name)
|
| 4920 |
if quarantine:
|
| 4921 |
return self.resolve_citations(value, quarantine=True)
|
| 4922 |
unknown = sorted({f"[{tag.upper()}]" for tag in re.findall(r'CITE-\d+', value, re.I)} - set(self.citation_map))
|
core/production_guard.py
CHANGED
|
@@ -572,6 +572,45 @@ def build_word_count_quality_report(diagnostics: list[dict[str, Any]] | None) ->
|
|
| 572 |
])
|
| 573 |
return "\n".join(lines).strip() + "\n"
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
def build_evidence_coverage_report(summary: dict[str, Any] | None) -> str:
|
| 576 |
"""Build a neutral reader-facing literature evidence coverage report.
|
| 577 |
|
|
@@ -644,6 +683,7 @@ def build_validated_package(
|
|
| 644 |
citation_audit: list[dict[str, Any]],
|
| 645 |
literature_metrics: dict[str, Any],
|
| 646 |
quality_diagnostics: list[dict[str, Any]] | None = None,
|
|
|
|
| 647 |
evidence_coverage: dict[str, Any] | None = None,
|
| 648 |
) -> PackageResult:
|
| 649 |
validate_markdown_manuscript(english_markdown, language_name="English")
|
|
@@ -680,6 +720,7 @@ def build_validated_package(
|
|
| 680 |
"CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
|
| 681 |
"LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
|
| 682 |
"WORD_COUNT_QUALITY_REPORT.txt": build_word_count_quality_report(quality_diagnostics).encode("utf-8"),
|
|
|
|
| 683 |
"EVIDENCE_COVERAGE_REPORT.txt": build_evidence_coverage_report(evidence_coverage).encode("utf-8"),
|
| 684 |
}
|
| 685 |
manifest = _manifest(entries)
|
|
|
|
| 572 |
])
|
| 573 |
return "\n".join(lines).strip() + "\n"
|
| 574 |
|
| 575 |
+
def build_quality_notes(notes: list[dict[str, Any]] | None) -> str:
|
| 576 |
+
"""Build a concise reader-facing record of nonfatal quality advisories.
|
| 577 |
+
|
| 578 |
+
C5.5 intentionally separates these notes from the manuscript itself. The
|
| 579 |
+
paper remains clean scholarly prose; only issues that do not alter core
|
| 580 |
+
study facts, inference, hypothesis identity, translation meaning, or file
|
| 581 |
+
integrity may appear here.
|
| 582 |
+
"""
|
| 583 |
+
items = [dict(item) for item in (notes or []) if isinstance(item, dict)]
|
| 584 |
+
lines = [
|
| 585 |
+
"SmartPaper Quality Notes",
|
| 586 |
+
"",
|
| 587 |
+
"Writing standard: Conciseness, Accuracy, and Rigor (CAR).",
|
| 588 |
+
"Policy: The manuscript was completed because the items below were judged nonfatal and did not alter the study's core content, main findings, research design, hypothesis identity, or file integrity.",
|
| 589 |
+
"These notes are for author review before formal submission and are intentionally kept outside the manuscript body.",
|
| 590 |
+
"",
|
| 591 |
+
]
|
| 592 |
+
advisory = [item for item in items if str(item.get("severity") or "").upper() == "ADVISORY"]
|
| 593 |
+
if not advisory:
|
| 594 |
+
lines.append("No nonfatal quality advisory was recorded.")
|
| 595 |
+
else:
|
| 596 |
+
lines.append("Items for author review:")
|
| 597 |
+
for index, item in enumerate(advisory, 1):
|
| 598 |
+
section = str(item.get("section") or "Manuscript")
|
| 599 |
+
issue = str(item.get("issue_type") or "quality_advisory")
|
| 600 |
+
detail = str(item.get("detail") or "A nonfatal quality issue was recorded.").strip()
|
| 601 |
+
action = str(item.get("user_action") or "Review before formal submission if additional polishing is desired.").strip()
|
| 602 |
+
lines.append(f"{index}. Section: {section}")
|
| 603 |
+
lines.append(f" Issue: {issue}")
|
| 604 |
+
lines.append(f" Note: {detail}")
|
| 605 |
+
lines.append(f" Suggested check: {action}")
|
| 606 |
+
lines.extend([
|
| 607 |
+
"",
|
| 608 |
+
"Important boundary:",
|
| 609 |
+
"Fatal integrity problems are not listed here because they must stop generation rather than be downgraded to advisory status.",
|
| 610 |
+
])
|
| 611 |
+
return "\n".join(lines).strip() + "\n"
|
| 612 |
+
|
| 613 |
+
|
| 614 |
def build_evidence_coverage_report(summary: dict[str, Any] | None) -> str:
|
| 615 |
"""Build a neutral reader-facing literature evidence coverage report.
|
| 616 |
|
|
|
|
| 683 |
citation_audit: list[dict[str, Any]],
|
| 684 |
literature_metrics: dict[str, Any],
|
| 685 |
quality_diagnostics: list[dict[str, Any]] | None = None,
|
| 686 |
+
quality_notes: list[dict[str, Any]] | None = None,
|
| 687 |
evidence_coverage: dict[str, Any] | None = None,
|
| 688 |
) -> PackageResult:
|
| 689 |
validate_markdown_manuscript(english_markdown, language_name="English")
|
|
|
|
| 720 |
"CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
|
| 721 |
"LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
|
| 722 |
"WORD_COUNT_QUALITY_REPORT.txt": build_word_count_quality_report(quality_diagnostics).encode("utf-8"),
|
| 723 |
+
"SmartPaper_Quality_Notes.txt": build_quality_notes(quality_notes).encode("utf-8"),
|
| 724 |
"EVIDENCE_COVERAGE_REPORT.txt": build_evidence_coverage_report(evidence_coverage).encode("utf-8"),
|
| 725 |
}
|
| 726 |
manifest = _manifest(entries)
|
core/quantitative_engine.py
CHANGED
|
@@ -602,40 +602,23 @@ Keywords: {keywords}"""
|
|
| 602 |
|
| 603 |
@classmethod
|
| 604 |
def _classify_quantitative_causal_clause(cls, clause):
|
| 605 |
-
"""Classify
|
| 606 |
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
causal
|
|
|
|
| 611 |
"""
|
| 612 |
value = re.sub(r"\s+", " ", str(clause or "")).strip().casefold()
|
| 613 |
if not value:
|
| 614 |
return "NON_CAUSAL"
|
| 615 |
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
rf"\b(?:is|are|was|were)\s+insufficient\s+to\s+(?:identify|establish|demonstrate|show|infer|support|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
|
| 620 |
-
rf"\b(?:is|are|was|were)\s+not\s+(?:designed|intended)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
|
| 621 |
-
rf"\bwithout\s+(?:identifying|establishing|demonstrating|showing|inferring|supporting|proving)\b[^.;]{{0,80}}\b{causal_nouns}\b",
|
| 622 |
-
rf"\b(?:no|without)\s+(?:evidence|basis|support|identification|proof)\b[^.;]{{0,60}}\b{causal_nouns}\b",
|
| 623 |
-
rf"\b(?:not|never)\s+(?:evidence|proof)\s+(?:of|for)\b[^.;]{{0,50}}\b{causal_nouns}\b",
|
| 624 |
-
rf"\b(?:should not|shouldn't|must not|cannot|can't|could not|couldn't)\s+be\s+(?:interpreted|read|treated|understood)\s+as\b[^.;]{{0,60}}\b{causal_nouns}\b",
|
| 625 |
-
rf"\b(?:is|are|was|were)\s+(?:restricted|limited|confined)\s+to\b[^.;]{{0,120}}\b(?:not|rather than|instead of)\s+(?:to\s+)?\b{causal_nouns}\b",
|
| 626 |
-
rf"\b(?:restricted|limited|confined)\s+to\b[^.;]{{0,120}}\b(?:not|rather than|instead of)\s+(?:to\s+)?\b{causal_nouns}\b",
|
| 627 |
-
rf"\b(?:association|associations|covariation|covariance|correlation|correlations|indirect association|descriptive pattern|descriptive patterns)\b[^.;]{{0,80}}\b(?:not(?:\s+as)?|rather than|instead of)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
|
| 628 |
-
rf"\b{causal_nouns}\b\s+(?:is|are|was|were)\s+(?:not|never)\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted|proven)\b",
|
| 629 |
-
rf"\b{causal_nouns}\b\s+(?:cannot|can't|could not|couldn't)\s+be\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted|proven)\b",
|
| 630 |
-
rf"\b(?:inability|lack\s+of\s+(?:an?\s+)?ability)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
|
| 631 |
-
rf"\b(?:not|no)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
|
| 632 |
-
rf"\b(?:cannot|can't|could not|couldn't)\s+be\s+used\s+to\s+(?:infer|establish|demonstrate|show|prove)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
|
| 633 |
-
rf"\bwithout\s+(?:proving|establishing|demonstrating|showing|identifying)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
|
| 634 |
)
|
| 635 |
-
|
| 636 |
-
return "EXPLICIT_CAUSAL_LIMITATION"
|
| 637 |
-
|
| 638 |
-
affirmative_patterns = (
|
| 639 |
r"\b(?:causes?|caused|causing)\b",
|
| 640 |
r"\b(?:leads?|led)\s+to\b",
|
| 641 |
r"\b(?:drives?|drove|driven)\b",
|
|
@@ -643,12 +626,91 @@ Keywords: {keywords}"""
|
|
| 643 |
r"\b(?:produces?|produced)\b",
|
| 644 |
r"\b(?:results?|resulted)\s+in\b",
|
| 645 |
r"\b(?:transmits?|transmitted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
|
|
|
|
| 646 |
r"\b(?:has|have|had|exerts?|exerted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
|
| 647 |
-
r"\bcausal\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships|identification)\b",
|
| 648 |
-
r"\b(?:demonstrates?|demonstrated|shows?|showed|establishes?|established|identifies?|identified|proves?|proved)\s+(?:a\s+|the\s+)?causal(?:ity|\s+(?:effect|effects|mechanism|pathway|relationship|relationships|mediation))\b",
|
| 649 |
)
|
| 650 |
-
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
return "NON_CAUSAL"
|
| 653 |
|
| 654 |
@classmethod
|
|
|
|
| 602 |
|
| 603 |
@classmethod
|
| 604 |
def _classify_quantitative_causal_clause(cls, clause):
|
| 605 |
+
"""Classify one clause by causal *assertion scope*, not token presence.
|
| 606 |
|
| 607 |
+
C5.5 freezes a single semantic contract for Writer/Validator/Retry. A
|
| 608 |
+
causal phrase inside an explicit limitation (for example, ``cannot
|
| 609 |
+
establish that X causes Y``) is not an affirmative claim. Conversely,
|
| 610 |
+
an actual causal assertion remains fail-closed even when another part
|
| 611 |
+
of the sentence contains a disclaimer.
|
| 612 |
"""
|
| 613 |
value = re.sub(r"\s+", " ", str(clause or "")).strip().casefold()
|
| 614 |
if not value:
|
| 615 |
return "NON_CAUSAL"
|
| 616 |
|
| 617 |
+
causal_term = (
|
| 618 |
+
r"(?:causal(?:ity|\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships|identification))|"
|
| 619 |
+
r"causation|cause(?:s|d|ing)?)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
)
|
| 621 |
+
direct_patterns = (
|
|
|
|
|
|
|
|
|
|
| 622 |
r"\b(?:causes?|caused|causing)\b",
|
| 623 |
r"\b(?:leads?|led)\s+to\b",
|
| 624 |
r"\b(?:drives?|drove|driven)\b",
|
|
|
|
| 626 |
r"\b(?:produces?|produced)\b",
|
| 627 |
r"\b(?:results?|resulted)\s+in\b",
|
| 628 |
r"\b(?:transmits?|transmitted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
|
| 629 |
+
r"\bcausally\s+(?:transmits?|mediates?|affects?|influences?)\b",
|
| 630 |
r"\b(?:has|have|had|exerts?|exerted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
|
|
|
|
|
|
|
| 631 |
)
|
| 632 |
+
|
| 633 |
+
def direct_match_is_disclaimed(match):
|
| 634 |
+
prefix = value[max(0, match.start() - 220):match.start()]
|
| 635 |
+
if re.search(
|
| 636 |
+
r"(?:\b(?:does|do|did|can|could|would|should|must|may|might)\s+not|"
|
| 637 |
+
r"\bcannot|\bcan't|\bcouldn't|\bnever)\s+(?:\w+\s+){0,2}$",
|
| 638 |
+
prefix,
|
| 639 |
+
flags=re.I,
|
| 640 |
+
):
|
| 641 |
+
return True
|
| 642 |
+
if re.search(
|
| 643 |
+
r"\b(?:cannot|can't|could not|couldn't|does not|doesn't|do not|don't|did not|didn't|"
|
| 644 |
+
r"fails? to|failed to|unable to|insufficient to|not able to)\s+"
|
| 645 |
+
r"(?:identify|establish|demonstrate|show|infer|support|imply|mean|claim|justify|warrant|prove)"
|
| 646 |
+
r"\b[^.;]{0,180}?(?:\bthat\b|\bwhether\b)[^.;]{0,120}$",
|
| 647 |
+
prefix,
|
| 648 |
+
flags=re.I,
|
| 649 |
+
):
|
| 650 |
+
return True
|
| 651 |
+
if re.search(
|
| 652 |
+
r"\b(?:should not|shouldn't|must not|cannot|can't|could not|couldn't)\s+be\s+"
|
| 653 |
+
r"(?:interpreted|read|treated|understood)\s+(?:as|to mean)\b[^.;]{0,140}$",
|
| 654 |
+
prefix,
|
| 655 |
+
flags=re.I,
|
| 656 |
+
):
|
| 657 |
+
return True
|
| 658 |
+
if re.search(
|
| 659 |
+
r"\bwithout\s+(?:identifying|establishing|demonstrating|showing|inferring|supporting|proving)"
|
| 660 |
+
r"\b[^.;]{0,180}?(?:\bthat\b|\bwhether\b)[^.;]{0,120}$",
|
| 661 |
+
prefix,
|
| 662 |
+
flags=re.I,
|
| 663 |
+
):
|
| 664 |
+
return True
|
| 665 |
+
return False
|
| 666 |
+
|
| 667 |
+
saw_disclaimed_direct = False
|
| 668 |
+
for pattern in direct_patterns:
|
| 669 |
+
for match in re.finditer(pattern, value, flags=re.I):
|
| 670 |
+
if direct_match_is_disclaimed(match):
|
| 671 |
+
saw_disclaimed_direct = True
|
| 672 |
+
continue
|
| 673 |
+
return "AFFIRMATIVE_CAUSAL"
|
| 674 |
+
|
| 675 |
+
affirmative_noun_patterns = (
|
| 676 |
+
rf"\b(?:is|are|was|were|represent(?:s|ed)?|constitut(?:es|ed))\s+(?:an?\s+|the\s+)?{causal_term}\b",
|
| 677 |
+
rf"\b(?:identify|identifies|identified|establish|establishes|established|demonstrate|demonstrates|demonstrated|show|shows|showed|prove|proves|proved|provide|provides|provided)\b[^.;]{{0,80}}\b{causal_term}\b",
|
| 678 |
+
rf"\b(?:evidence|proof)\s+(?:of|for)\s+(?:an?\s+|the\s+)?{causal_term}\b",
|
| 679 |
+
rf"\b{causal_term}\b[^.;]{{0,80}}\b(?:is|are|was|were)\s+(?!not\b|never\b)",
|
| 680 |
+
)
|
| 681 |
+
for pattern in affirmative_noun_patterns:
|
| 682 |
+
for match in re.finditer(pattern, value, flags=re.I):
|
| 683 |
+
prefix = value[max(0, match.start() - 80):match.start()]
|
| 684 |
+
if re.search(
|
| 685 |
+
r"\b(?:no|not|without|cannot|can't|insufficient|unable|does not|doesn't|do not|don't|did not|didn't|not able)\b",
|
| 686 |
+
prefix,
|
| 687 |
+
flags=re.I,
|
| 688 |
+
):
|
| 689 |
+
continue
|
| 690 |
+
return "AFFIRMATIVE_CAUSAL"
|
| 691 |
+
|
| 692 |
+
limitation_patterns = (
|
| 693 |
+
rf"\b(?:cannot|can't|could not|couldn't|does not|doesn't|do not|don't|did not|didn't|fails? to|failed to|"
|
| 694 |
+
rf"is not able to|are not able to|was not able to|were not able to|unable to|insufficient to|not able to)\s+"
|
| 695 |
+
rf"(?:identify|establish|demonstrate|show|infer|support|imply|mean|claim|justify|warrant|permit|allow|prove)\b[^.;]*\b{causal_term}\b",
|
| 696 |
+
rf"\b(?:is|are|was|were)\s+not\s+(?:designed|intended)\s+to\s+"
|
| 697 |
+
rf"(?:identify|establish|demonstrate|show|infer|support|prove)\b[^.;]*\b{causal_term}\b",
|
| 698 |
+
rf"\bwithout\s+(?:identifying|establishing|demonstrating|showing|inferring|supporting|proving)\b[^.;]*\b{causal_term}\b",
|
| 699 |
+
rf"\b(?:no|without)\s+(?:evidence|basis|support|identification|proof)\b[^.;]*\b{causal_term}\b",
|
| 700 |
+
rf"\b(?:is|are|was|were)\s+not\s+(?:evidence|proof)\s+(?:of|for)\s+(?:an?\s+|the\s+)?{causal_term}\b",
|
| 701 |
+
rf"\b(?:cannot|can't|could not|couldn't)\s+be\s+used\s+to\s+(?:infer|establish|demonstrate|show|prove)\b[^.;]*\b{causal_term}\b",
|
| 702 |
+
rf"\b(?:should not|shouldn't|must not|cannot|can't|could not|couldn't)\s+be\s+"
|
| 703 |
+
rf"(?:interpreted|read|treated|understood)\s+(?:as|to mean)\b[^.;]*\b{causal_term}\b",
|
| 704 |
+
rf"\b(?:is|are|was|were)?\s*(?:restricted|limited|confined)\s+to\b[^.;]*"
|
| 705 |
+
rf"\b(?:not|rather than|instead of)\s+(?:to\s+)?(?:an?\s+|the\s+)?{causal_term}\b",
|
| 706 |
+
rf"\b(?:association|associations|covariation|covariance|correlation|correlations|indirect association|descriptive pattern|descriptive patterns)\b[^.;]*"
|
| 707 |
+
rf"\b(?:not(?:\s+as)?|rather than|instead of)\s+(?:an?\s+|the\s+)?{causal_term}\b",
|
| 708 |
+
rf"\b{causal_term}\b\s+(?:is|are|was|were)\s+(?:not|never)\s+"
|
| 709 |
+
rf"(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted|proven)\b",
|
| 710 |
+
rf"\b(?:not|no)\s+(?:an?\s+|the\s+)?{causal_term}\b",
|
| 711 |
+
)
|
| 712 |
+
if saw_disclaimed_direct or any(re.search(pattern, value, flags=re.I) for pattern in limitation_patterns):
|
| 713 |
+
return "EXPLICIT_CAUSAL_LIMITATION"
|
| 714 |
return "NON_CAUSAL"
|
| 715 |
|
| 716 |
@classmethod
|