Spaces:
Running
Running
S0-R1-C5.5-R6-R10 Quantitative Method Citation Retry Frame
#98
by GuXiaopeng - opened
- core/generation_runner.py +102 -7
- core/pipeline_agent.py +25 -4
- core/quantitative_engine.py +124 -10
core/generation_runner.py
CHANGED
|
@@ -1070,6 +1070,47 @@ class GenerationRunner:
|
|
| 1070 |
return lineage
|
| 1071 |
return kind
|
| 1072 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
@classmethod
|
| 1074 |
def _section_retry_decision(cls, rows, lineage_attempts, *, hard_cap_reached: bool = False):
|
| 1075 |
"""Return (may_retry, exhausted_lineages, new_lineages).
|
|
@@ -2995,7 +3036,12 @@ class GenerationRunner:
|
|
| 2995 |
self._build_qualitative_quote_invocation_plan(adaptive_structure, data_metadata)
|
| 2996 |
if mode == "asq" else {}
|
| 2997 |
)
|
| 2998 |
-
jit_texts = agent.prefetch_jit_literature(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2999 |
if jit_texts:
|
| 3000 |
rag_engine._initialize_chunks([jit_texts])
|
| 3001 |
self._progress(15, "citation_planning")
|
|
@@ -3690,9 +3736,11 @@ class GenerationRunner:
|
|
| 3690 |
"\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. "
|
| 3691 |
"Do not invent additional literature claims merely to make the section sound comprehensive. Prefer concise study positioning, cautious conceptual reasoning, and clearly qualified interpretations when no directly validated source is assigned."
|
| 3692 |
)
|
| 3693 |
-
memory_context =
|
|
|
|
|
|
|
| 3694 |
memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
|
| 3695 |
-
snippets =
|
| 3696 |
if snippets:
|
| 3697 |
memory_block += f"\n<TARGETED_RAG>\n{snippets}\n</TARGETED_RAG>\n"
|
| 3698 |
heading_directive = "Do not output the section heading; it will be inserted by the system."
|
|
@@ -3803,7 +3851,10 @@ class GenerationRunner:
|
|
| 3803 |
"When no supported external claim is assigned, prefer concise study positioning, transparent limitations, and cautious conceptual reasoning without invented citation support."
|
| 3804 |
)
|
| 3805 |
else:
|
| 3806 |
-
|
|
|
|
|
|
|
|
|
|
| 3807 |
user_prompt = f"""Write the assigned English manuscript unit.
|
| 3808 |
Section: {sec_title}
|
| 3809 |
Subsection: {subsection or 'None'}
|
|
@@ -3861,6 +3912,14 @@ Output only final academic prose. Do not report the word count."""
|
|
| 3861 |
# Four is a hard safety cap, not a blanket four-attempt entitlement.
|
| 3862 |
max_section_attempts = 1 if deterministic_appendix else 4
|
| 3863 |
lineage_attempts = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3864 |
frozen_citation_state = self._snapshot_section_citation_state(
|
| 3865 |
agent, citation_plan, display_name
|
| 3866 |
)
|
|
@@ -3897,6 +3956,13 @@ Output only final academic prose. Do not report the word count."""
|
|
| 3897 |
f"<CLAIM_EVIDENCE_PLAN>\n{retry_claim_plan}\n</CLAIM_EVIDENCE_PLAN>",
|
| 3898 |
candidate_user_prompt, flags=re.S,
|
| 3899 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3900 |
if attempt_feedback:
|
| 3901 |
excluded_text = ", ".join(sorted(retry_excluded_tags, key=agent._citation_tag_number)) or "None"
|
| 3902 |
candidate_user_prompt += f"""
|
|
@@ -3995,20 +4061,49 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 3995 |
preview_blocking, preview_advisory = self._partition_section_violations(
|
| 3996 |
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=False
|
| 3997 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3998 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 3999 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 4000 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4001 |
if may_retry:
|
| 4002 |
retry_rows = [
|
| 4003 |
row for row in preview_blocking
|
| 4004 |
-
if self._section_violation_lineage(row) in
|
| 4005 |
]
|
| 4006 |
for lineage in fresh_lineages:
|
| 4007 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4008 |
attempt_feedback = self._section_retry_feedback(retry_rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4009 |
logging.warning(
|
| 4010 |
-
"Unified Phase-1 Section lineage retry | unit=%s attempt=%s lineages=%s exhausted_siblings=%s count=%s types=%s",
|
| 4011 |
-
display_name, section_attempt, sorted(
|
|
|
|
| 4012 |
sorted({str(row.get("type") or "") for row in retry_rows if isinstance(row, dict)}),
|
| 4013 |
)
|
| 4014 |
self._restore_section_citation_state(
|
|
|
|
| 1070 |
return lineage
|
| 1071 |
return kind
|
| 1072 |
|
| 1073 |
+
@staticmethod
|
| 1074 |
+
def _strip_unscoped_citation_identities(value):
|
| 1075 |
+
"""Remove raw citation capability tokens from non-authoritative context."""
|
| 1076 |
+
return re.sub(r"\[CITE-\d+\]", "", str(value or ""), flags=re.I)
|
| 1077 |
+
|
| 1078 |
+
@staticmethod
|
| 1079 |
+
def _section_citation_capability_context(agent, planned_items, max_chars=12000):
|
| 1080 |
+
"""Expose only citation identities already frozen for this manuscript unit.
|
| 1081 |
+
|
| 1082 |
+
Retrieval visibility is not citation authority. R6-R10 therefore builds
|
| 1083 |
+
writer-facing targeted context from the unit's frozen claim-evidence plan
|
| 1084 |
+
instead of the global RAG pool, which may contain METHOD_AUTHORITY tags
|
| 1085 |
+
owned by another unit.
|
| 1086 |
+
"""
|
| 1087 |
+
rows = []
|
| 1088 |
+
seen = set()
|
| 1089 |
+
for item in list(planned_items or []):
|
| 1090 |
+
if not isinstance(item, dict):
|
| 1091 |
+
continue
|
| 1092 |
+
tag = str(item.get("tag") or "").strip()
|
| 1093 |
+
if not tag or tag in seen:
|
| 1094 |
+
continue
|
| 1095 |
+
metadata = dict(getattr(agent, "citation_map", {}).get(tag) or {})
|
| 1096 |
+
evidence = str(item.get("evidence") or metadata.get("evidence") or "").strip()
|
| 1097 |
+
claim = str(item.get("claim") or "").strip()
|
| 1098 |
+
if not evidence:
|
| 1099 |
+
continue
|
| 1100 |
+
seen.add(tag)
|
| 1101 |
+
row = f"{tag} AUTHORIZED CLAIM: {claim}\nEVIDENCE: {evidence}" if claim else f"{tag} EVIDENCE: {evidence}"
|
| 1102 |
+
rows.append(row)
|
| 1103 |
+
value = "\n\n--- Authorized Section Evidence ---\n".join(rows)
|
| 1104 |
+
return value[:max(0, int(max_chars))]
|
| 1105 |
+
|
| 1106 |
+
@staticmethod
|
| 1107 |
+
def _section_regressed_lineages(current_lineages, cleared_lineages, regression_attempts):
|
| 1108 |
+
"""Return previously-cleared lineages reintroduced by a later sibling repair."""
|
| 1109 |
+
current = {str(value) for value in (current_lineages or set()) if str(value)}
|
| 1110 |
+
cleared = {str(value) for value in (cleared_lineages or set()) if str(value)}
|
| 1111 |
+
attempts = dict(regression_attempts or {})
|
| 1112 |
+
return {name for name in current & cleared if int(attempts.get(name, 0)) < 1}
|
| 1113 |
+
|
| 1114 |
@classmethod
|
| 1115 |
def _section_retry_decision(cls, rows, lineage_attempts, *, hard_cap_reached: bool = False):
|
| 1116 |
"""Return (may_retry, exhausted_lineages, new_lineages).
|
|
|
|
| 3036 |
self._build_qualitative_quote_invocation_plan(adaptive_structure, data_metadata)
|
| 3037 |
if mode == "asq" else {}
|
| 3038 |
)
|
| 3039 |
+
jit_texts = agent.prefetch_jit_literature(
|
| 3040 |
+
topic, keywords, adaptive_structure,
|
| 3041 |
+
quantitative_evidence_ledger=(
|
| 3042 |
+
data_metadata.get("quantitative_evidence_ledger") if mode == "amj" else None
|
| 3043 |
+
),
|
| 3044 |
+
)
|
| 3045 |
if jit_texts:
|
| 3046 |
rag_engine._initialize_chunks([jit_texts])
|
| 3047 |
self._progress(15, "citation_planning")
|
|
|
|
| 3736 |
"\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. "
|
| 3737 |
"Do not invent additional literature claims merely to make the section sound comprehensive. Prefer concise study positioning, cautious conceptual reasoning, and clearly qualified interpretations when no directly validated source is assigned."
|
| 3738 |
)
|
| 3739 |
+
memory_context = self._strip_unscoped_citation_identities(
|
| 3740 |
+
agent._build_memory_context(sec_title, 2500)
|
| 3741 |
+
)
|
| 3742 |
memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
|
| 3743 |
+
snippets = self._section_citation_capability_context(agent, planned_items)
|
| 3744 |
if snippets:
|
| 3745 |
memory_block += f"\n<TARGETED_RAG>\n{snippets}\n</TARGETED_RAG>\n"
|
| 3746 |
heading_directive = "Do not output the section heading; it will be inserted by the system."
|
|
|
|
| 3851 |
"When no supported external claim is assigned, prefer concise study positioning, transparent limitations, and cautious conceptual reasoning without invented citation support."
|
| 3852 |
)
|
| 3853 |
else:
|
| 3854 |
+
# Literature synthesis is planning context, not a citation capability.
|
| 3855 |
+
# Raw identities are stripped; only the frozen Section plan below may
|
| 3856 |
+
# expose a usable [CITE-XX] token to the writer.
|
| 3857 |
+
writer_synthesis_context = self._strip_unscoped_citation_identities(synthesis_result)
|
| 3858 |
user_prompt = f"""Write the assigned English manuscript unit.
|
| 3859 |
Section: {sec_title}
|
| 3860 |
Subsection: {subsection or 'None'}
|
|
|
|
| 3912 |
# Four is a hard safety cap, not a blanket four-attempt entitlement.
|
| 3913 |
max_section_attempts = 1 if deterministic_appendix else 4
|
| 3914 |
lineage_attempts = {}
|
| 3915 |
+
# R6-R10 monotonic retry state. A defect that was repaired and absent
|
| 3916 |
+
# on an intervening attempt may receive one regression-correction turn
|
| 3917 |
+
# if a later sibling repair reintroduces it. The Section hard cap is
|
| 3918 |
+
# unchanged, so this cannot create an unbounded retry loop.
|
| 3919 |
+
seen_lineages = set()
|
| 3920 |
+
cleared_lineages = set()
|
| 3921 |
+
regression_attempts = {}
|
| 3922 |
+
historical_retry_rows = {}
|
| 3923 |
frozen_citation_state = self._snapshot_section_citation_state(
|
| 3924 |
agent, citation_plan, display_name
|
| 3925 |
)
|
|
|
|
| 3956 |
f"<CLAIM_EVIDENCE_PLAN>\n{retry_claim_plan}\n</CLAIM_EVIDENCE_PLAN>",
|
| 3957 |
candidate_user_prompt, flags=re.S,
|
| 3958 |
)
|
| 3959 |
+
retry_capability_context = self._section_citation_capability_context(agent, planned_items)
|
| 3960 |
+
candidate_user_prompt = re.sub(
|
| 3961 |
+
r"\n<TARGETED_RAG>.*?</TARGETED_RAG>\n",
|
| 3962 |
+
(f"\n<TARGETED_RAG>\n{retry_capability_context}\n</TARGETED_RAG>\n"
|
| 3963 |
+
if retry_capability_context else "\n"),
|
| 3964 |
+
candidate_user_prompt, flags=re.S,
|
| 3965 |
+
)
|
| 3966 |
if attempt_feedback:
|
| 3967 |
excluded_text = ", ".join(sorted(retry_excluded_tags, key=agent._citation_tag_number)) or "None"
|
| 3968 |
candidate_user_prompt += f"""
|
|
|
|
| 4061 |
preview_blocking, preview_advisory = self._partition_section_violations(
|
| 4062 |
phase1_validation_rows, section_name=display_name, mode=mode, exhausted=False
|
| 4063 |
)
|
| 4064 |
+
current_lineages = {
|
| 4065 |
+
self._section_violation_lineage(row) for row in preview_blocking
|
| 4066 |
+
}
|
| 4067 |
+
cleared_lineages.update(seen_lineages - current_lineages)
|
| 4068 |
+
regressed_lineages = self._section_regressed_lineages(
|
| 4069 |
+
current_lineages, cleared_lineages, regression_attempts
|
| 4070 |
+
)
|
| 4071 |
+
seen_lineages.update(current_lineages)
|
| 4072 |
may_retry, exhausted_lineages, fresh_lineages = self._section_retry_decision(
|
| 4073 |
preview_blocking, lineage_attempts, hard_cap_reached=hard_cap_reached
|
| 4074 |
)
|
| 4075 |
+
retry_lineages = set(fresh_lineages)
|
| 4076 |
+
if regressed_lineages and not hard_cap_reached:
|
| 4077 |
+
retry_lineages.update(regressed_lineages)
|
| 4078 |
+
may_retry = True
|
| 4079 |
if may_retry:
|
| 4080 |
retry_rows = [
|
| 4081 |
row for row in preview_blocking
|
| 4082 |
+
if self._section_violation_lineage(row) in retry_lineages
|
| 4083 |
]
|
| 4084 |
for lineage in fresh_lineages:
|
| 4085 |
lineage_attempts[lineage] = int(lineage_attempts.get(lineage, 0)) + 1
|
| 4086 |
+
for lineage in regressed_lineages:
|
| 4087 |
+
regression_attempts[lineage] = int(regression_attempts.get(lineage, 0)) + 1
|
| 4088 |
+
for row in preview_blocking:
|
| 4089 |
+
historical_retry_rows[self._section_violation_lineage(row)] = dict(row)
|
| 4090 |
+
# Every later repair is constrained by all earlier resolved
|
| 4091 |
+
# contracts, not only the newly visible lineage. This makes
|
| 4092 |
+
# A+B -> C -> A+B repair interference explicitly non-monotonic
|
| 4093 |
+
# and gives it one bounded correction turn.
|
| 4094 |
attempt_feedback = self._section_retry_feedback(retry_rows)
|
| 4095 |
+
historical_feedback = self._section_retry_feedback(
|
| 4096 |
+
list(historical_retry_rows.values())
|
| 4097 |
+
)
|
| 4098 |
+
if historical_feedback:
|
| 4099 |
+
attempt_feedback += (
|
| 4100 |
+
"\nNONREGRESSION CONSTRAINTS — previously repaired lineages must remain resolved:\n"
|
| 4101 |
+
+ historical_feedback
|
| 4102 |
+
)
|
| 4103 |
logging.warning(
|
| 4104 |
+
"Unified Phase-1 Section lineage retry | unit=%s attempt=%s lineages=%s exhausted_siblings=%s regressed_lineages=%s count=%s types=%s",
|
| 4105 |
+
display_name, section_attempt, sorted(retry_lineages), sorted(exhausted_lineages),
|
| 4106 |
+
sorted(regressed_lineages), len(retry_rows),
|
| 4107 |
sorted({str(row.get("type") or "") for row in retry_rows if isinstance(row, dict)}),
|
| 4108 |
)
|
| 4109 |
self._restore_section_citation_state(
|
core/pipeline_agent.py
CHANGED
|
@@ -2311,8 +2311,27 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2311 |
return violations
|
| 2312 |
|
| 2313 |
@staticmethod
|
| 2314 |
-
def _method_authority_queries(structure):
|
| 2315 |
-
"""Derive bounded methodology queries from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2316 |
text_parts = []
|
| 2317 |
for sec in structure or []:
|
| 2318 |
if not isinstance(sec, dict):
|
|
@@ -2350,10 +2369,12 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2350 |
queries.append('empirical research design methodology')
|
| 2351 |
return list(dict.fromkeys(queries))[:3]
|
| 2352 |
|
| 2353 |
-
def prefetch_jit_literature(self, topic, keywords, structure):
|
| 2354 |
"""Retrieve a dedicated methodology-authority lane without an LLM query call."""
|
| 2355 |
jit_rag_texts = []
|
| 2356 |
-
queries_to_fetch = self._method_authority_queries(
|
|
|
|
|
|
|
| 2357 |
if not queries_to_fetch:
|
| 2358 |
return ""
|
| 2359 |
logging.info("METHOD_AUTHORITY retrieval | queries=%s", queries_to_fetch)
|
|
|
|
| 2311 |
return violations
|
| 2312 |
|
| 2313 |
@staticmethod
|
| 2314 |
+
def _method_authority_queries(structure, quantitative_evidence_ledger=None):
|
| 2315 |
+
"""Derive bounded methodology queries from the *owned* evidence contract.
|
| 2316 |
+
|
| 2317 |
+
R6-R10 prevents generic Methodology headings from retrieving methods the
|
| 2318 |
+
study did not perform (for example SEM for a VIF/bootstrap design).
|
| 2319 |
+
"""
|
| 2320 |
+
ledger = dict(quantitative_evidence_ledger or {})
|
| 2321 |
+
if bool(ledger.get("strict")):
|
| 2322 |
+
allowed = {str(value or "").strip().casefold() for value in (ledger.get("allowed_methods") or [])}
|
| 2323 |
+
queries = []
|
| 2324 |
+
if allowed & {"reliability", "composite reliability", "average variance extracted", "fornell-larcker"}:
|
| 2325 |
+
queries.append("measurement reliability validity methodology")
|
| 2326 |
+
if "vif" in allowed:
|
| 2327 |
+
queries.append("variance inflation factor collinearity methodology")
|
| 2328 |
+
if "bootstrap" in allowed:
|
| 2329 |
+
queries.append("bootstrap inference methodology")
|
| 2330 |
+
if "spearman" in allowed:
|
| 2331 |
+
queries.append("Spearman rank correlation methodology")
|
| 2332 |
+
if not queries:
|
| 2333 |
+
queries.append("empirical association analysis methodology")
|
| 2334 |
+
return list(dict.fromkeys(queries))[:3]
|
| 2335 |
text_parts = []
|
| 2336 |
for sec in structure or []:
|
| 2337 |
if not isinstance(sec, dict):
|
|
|
|
| 2369 |
queries.append('empirical research design methodology')
|
| 2370 |
return list(dict.fromkeys(queries))[:3]
|
| 2371 |
|
| 2372 |
+
def prefetch_jit_literature(self, topic, keywords, structure, quantitative_evidence_ledger=None):
|
| 2373 |
"""Retrieve a dedicated methodology-authority lane without an LLM query call."""
|
| 2374 |
jit_rag_texts = []
|
| 2375 |
+
queries_to_fetch = self._method_authority_queries(
|
| 2376 |
+
structure, quantitative_evidence_ledger=quantitative_evidence_ledger
|
| 2377 |
+
)
|
| 2378 |
if not queries_to_fetch:
|
| 2379 |
return ""
|
| 2380 |
logging.info("METHOD_AUTHORITY retrieval | queries=%s", queries_to_fetch)
|
core/quantitative_engine.py
CHANGED
|
@@ -861,6 +861,98 @@ Keywords: {keywords}"""
|
|
| 861 |
] if strict else [],
|
| 862 |
}
|
| 863 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
@staticmethod
|
| 865 |
def _semantic_frame_hash(payload):
|
| 866 |
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
@@ -1345,7 +1437,12 @@ Keywords: {keywords}"""
|
|
| 1345 |
|
| 1346 |
for _start, _end, sentence in cls._sentence_spans(value):
|
| 1347 |
ownership = cls._quantitative_sentence_ownership(sentence, section_name, known_constructs)
|
| 1348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
continue
|
| 1350 |
if sample_size:
|
| 1351 |
for match in re.finditer(r"(?i)(\b(?:sample(?:\s+size)?(?:\s+of)?|N)\s*(?:=|:)?\s*)(\d{2,6})\b", sentence):
|
|
@@ -1392,10 +1489,16 @@ Keywords: {keywords}"""
|
|
| 1392 |
})
|
| 1393 |
if (any(c in sentence.casefold() for c in construct_folded) if construct_folded else False) and re.search(r"(?i)\bp\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", sentence):
|
| 1394 |
violations.append({"type":"unfrozen_p_value","preview":sentence.strip()[:180]})
|
| 1395 |
-
if ownership == "study":
|
| 1396 |
-
|
| 1397 |
-
if
|
| 1398 |
-
violations.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1399 |
|
| 1400 |
vif_expected = results.get("predictor_mediator_vif") if strict else uploaded.get("VIF")
|
| 1401 |
boot_expected = results.get("bootstrap_iterations") if strict else uploaded.get("bootstrap_iterations")
|
|
@@ -1474,7 +1577,12 @@ Keywords: {keywords}"""
|
|
| 1474 |
replacements = []
|
| 1475 |
for start, end, sentence in cls._sentence_spans(value):
|
| 1476 |
ownership = cls._quantitative_sentence_ownership(sentence, section_name, known_constructs)
|
| 1477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1478 |
continue
|
| 1479 |
revised = sentence
|
| 1480 |
|
|
@@ -1533,10 +1641,16 @@ Keywords: {keywords}"""
|
|
| 1533 |
revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
|
| 1534 |
if p_count:
|
| 1535 |
diagnostics.append({"type":"unfrozen_p_value_removed","count":p_count})
|
| 1536 |
-
if ownership == "study":
|
| 1537 |
-
|
| 1538 |
-
if
|
| 1539 |
-
diagnostics.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1540 |
revised = ""
|
| 1541 |
|
| 1542 |
def replace_local_metric(pattern, expected, *, dtype="float", label="metric"):
|
|
|
|
| 861 |
] if strict else [],
|
| 862 |
}
|
| 863 |
|
| 864 |
+
@classmethod
|
| 865 |
+
def classify_quantitative_method_threat_claim(cls, sentence, term):
|
| 866 |
+
"""Classify a forbidden quantitative term by *claim role*, not token presence.
|
| 867 |
+
|
| 868 |
+
R6-R10 separates an actually performed/claimed unowned analysis from a
|
| 869 |
+
limitation, explicit non-use statement, threat discussion, or incidental
|
| 870 |
+
methodological mention. Only ``PERFORMED_UNOWNED`` is blocking.
|
| 871 |
+
"""
|
| 872 |
+
text = re.sub(r"\s+", " ", str(sentence or "")).strip()
|
| 873 |
+
folded = text.casefold()
|
| 874 |
+
label = re.sub(r"\s+", " ", str(term or "")).strip()
|
| 875 |
+
term_folded = label.casefold()
|
| 876 |
+
if not text or not term_folded:
|
| 877 |
+
return {"term": label, "category": "UNKNOWN", "state": "ABSENT"}
|
| 878 |
+
match = re.search(rf"(?i)(?<![A-Za-z]){re.escape(label)}(?![A-Za-z])", text)
|
| 879 |
+
if not match:
|
| 880 |
+
return {"term": label, "category": "UNKNOWN", "state": "ABSENT"}
|
| 881 |
+
|
| 882 |
+
threat_terms = {"common method bias", "endogeneity"}
|
| 883 |
+
fit_terms = {"rmsea", "cfi", "tli", "srmr"}
|
| 884 |
+
category = (
|
| 885 |
+
"THREAT" if term_folded in threat_terms else
|
| 886 |
+
"FIT_DIAGNOSTIC" if term_folded in fit_terms else
|
| 887 |
+
"ANALYTIC_METHOD"
|
| 888 |
+
)
|
| 889 |
+
before = folded[max(0, match.start() - 100):match.start()]
|
| 890 |
+
after = folded[match.end():min(len(folded), match.end() + 120)]
|
| 891 |
+
local = folded[max(0, match.start() - 120):min(len(folded), match.end() + 140)]
|
| 892 |
+
|
| 893 |
+
scalar_negation = bool(re.search(
|
| 894 |
+
r"\bnot\s+(?:only|merely|just|simply)\b", local
|
| 895 |
+
))
|
| 896 |
+
explicit_nonuse = (not scalar_negation) and bool(
|
| 897 |
+
re.search(
|
| 898 |
+
r"\b(?:no|not|never|without|did\s+not|does\s+not|do\s+not|was\s+not|were\s+not|is\s+not|are\s+not|cannot|can\s+not)\b[^.;:]{0,55}$",
|
| 899 |
+
before,
|
| 900 |
+
)
|
| 901 |
+
or re.search(
|
| 902 |
+
r"^.{0,45}\b(?:was\s+not|were\s+not|is\s+not|are\s+not|did\s+not|does\s+not|do\s+not|cannot|can\s+not|not\s+(?:used|conducted|performed|estimated|tested|assessed|evaluated|reported|implemented|applied))\b",
|
| 903 |
+
after,
|
| 904 |
+
)
|
| 905 |
+
or re.search(
|
| 906 |
+
r"\b(?:does\s+not|do\s+not|did\s+not|cannot|can\s+not|is\s+not|are\s+not|was\s+not|were\s+not)\s+(?:address|assess|test|establish|identify|rule\s+out|correct|control|substitute|demonstrate|show|prove|support)\b",
|
| 907 |
+
local,
|
| 908 |
+
)
|
| 909 |
+
)
|
| 910 |
+
if explicit_nonuse:
|
| 911 |
+
return {"term": label, "category": category, "state": "NOT_PERFORMED_OR_LIMITATION"}
|
| 912 |
+
|
| 913 |
+
performed = bool(
|
| 914 |
+
re.search(
|
| 915 |
+
r"\b(?:we|the\s+study|this\s+study|the\s+analysis|our\s+analysis|the\s+model|the\s+authors?)\b[^.;:]{0,70}\b(?:use|uses|used|employ|employs|employed|conduct|conducts|conducted|perform|performs|performed|estimate|estimates|estimated|test|tests|tested|assess|assesses|assessed|evaluate|evaluates|evaluated|implement|implements|implemented|apply|applies|applied|ran|run|runs|fit|fits|fitted|model|models|modeled|modelled|control|controls|controlled|correct|corrects|corrected|address|addresses|addressed)\b[^.;:]{0,70}$",
|
| 916 |
+
before,
|
| 917 |
+
)
|
| 918 |
+
or re.search(
|
| 919 |
+
r"^.{0,65}\b(?:was|were|is|are)\s+(?:used|employed|conducted|performed|estimated|tested|assessed|evaluated|implemented|applied|fit|fitted|modeled|modelled|controlled|corrected|addressed|reported)\b",
|
| 920 |
+
after,
|
| 921 |
+
)
|
| 922 |
+
or re.search(
|
| 923 |
+
r"^.{0,65}\b(?:estimated|tested|assessed|evaluated|showed|indicated|yielded|produced|confirmed|supported)\b",
|
| 924 |
+
after,
|
| 925 |
+
)
|
| 926 |
+
)
|
| 927 |
+
if category == "FIT_DIAGNOSTIC" and re.search(
|
| 928 |
+
rf"(?i)(?:{re.escape(label)}\s*(?:=|:|was|were)\s*[-+]?\d)|(?:reported|fit\s+index|fit\s+indices)[^.;:]{{0,45}}{re.escape(label)}",
|
| 929 |
+
text,
|
| 930 |
+
):
|
| 931 |
+
performed = True
|
| 932 |
+
if category == "THREAT":
|
| 933 |
+
# Threats may be discussed as limitations without implying a test.
|
| 934 |
+
if re.search(
|
| 935 |
+
rf"(?i)(?:assess|test|evaluate|examine|address|correct|control)(?:ed|ing|s)?[^.;:]{{0,45}}{re.escape(label)}|{re.escape(label)}[^.;:]{{0,45}}(?:was|were)\s+(?:assessed|tested|evaluated|examined|addressed|corrected|controlled)",
|
| 936 |
+
text,
|
| 937 |
+
):
|
| 938 |
+
performed = True
|
| 939 |
+
elif not performed:
|
| 940 |
+
return {"term": label, "category": category, "state": "LIMITATION_OR_THREAT"}
|
| 941 |
+
|
| 942 |
+
return {
|
| 943 |
+
"term": label,
|
| 944 |
+
"category": category,
|
| 945 |
+
"state": "PERFORMED_UNOWNED" if performed else "MENTION_ONLY",
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
@classmethod
|
| 949 |
+
def _first_performed_unowned_quantitative_term(cls, sentence, forbidden_terms):
|
| 950 |
+
for term in forbidden_terms or []:
|
| 951 |
+
frame = cls.classify_quantitative_method_threat_claim(sentence, term)
|
| 952 |
+
if frame.get("state") == "PERFORMED_UNOWNED":
|
| 953 |
+
return frame
|
| 954 |
+
return None
|
| 955 |
+
|
| 956 |
@staticmethod
|
| 957 |
def _semantic_frame_hash(payload):
|
| 958 |
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
|
|
| 1437 |
|
| 1438 |
for _start, _end, sentence in cls._sentence_spans(value):
|
| 1439 |
ownership = cls._quantitative_sentence_ownership(sentence, section_name, known_constructs)
|
| 1440 |
+
method_scope_unknown = (
|
| 1441 |
+
strict and ownership == "unknown"
|
| 1442 |
+
and bool(re.search(r"(?i)method|measure", str(section_name or "")))
|
| 1443 |
+
and cls._first_performed_unowned_quantitative_term(sentence, forbidden) is not None
|
| 1444 |
+
)
|
| 1445 |
+
if ownership not in {"study", "result"} and not method_scope_unknown:
|
| 1446 |
continue
|
| 1447 |
if sample_size:
|
| 1448 |
for match in re.finditer(r"(?i)(\b(?:sample(?:\s+size)?(?:\s+of)?|N)\s*(?:=|:)?\s*)(\d{2,6})\b", sentence):
|
|
|
|
| 1489 |
})
|
| 1490 |
if (any(c in sentence.casefold() for c in construct_folded) if construct_folded else False) and re.search(r"(?i)\bp\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", sentence):
|
| 1491 |
violations.append({"type":"unfrozen_p_value","preview":sentence.strip()[:180]})
|
| 1492 |
+
if ownership == "study" or method_scope_unknown:
|
| 1493 |
+
method_frame = cls._first_performed_unowned_quantitative_term(sentence, forbidden)
|
| 1494 |
+
if method_frame:
|
| 1495 |
+
violations.append({
|
| 1496 |
+
"type":"unsupported_quantitative_method",
|
| 1497 |
+
"method":method_frame.get("term"),
|
| 1498 |
+
"claim_state":method_frame.get("state"),
|
| 1499 |
+
"method_category":method_frame.get("category"),
|
| 1500 |
+
"preview":sentence.strip()[:180],
|
| 1501 |
+
})
|
| 1502 |
|
| 1503 |
vif_expected = results.get("predictor_mediator_vif") if strict else uploaded.get("VIF")
|
| 1504 |
boot_expected = results.get("bootstrap_iterations") if strict else uploaded.get("bootstrap_iterations")
|
|
|
|
| 1577 |
replacements = []
|
| 1578 |
for start, end, sentence in cls._sentence_spans(value):
|
| 1579 |
ownership = cls._quantitative_sentence_ownership(sentence, section_name, known_constructs)
|
| 1580 |
+
method_scope_unknown = (
|
| 1581 |
+
strict and ownership == "unknown"
|
| 1582 |
+
and bool(re.search(r"(?i)method|measure", str(section_name or "")))
|
| 1583 |
+
and cls._first_performed_unowned_quantitative_term(sentence, forbidden) is not None
|
| 1584 |
+
)
|
| 1585 |
+
if ownership not in {"study", "result"} and not method_scope_unknown:
|
| 1586 |
continue
|
| 1587 |
revised = sentence
|
| 1588 |
|
|
|
|
| 1641 |
revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
|
| 1642 |
if p_count:
|
| 1643 |
diagnostics.append({"type":"unfrozen_p_value_removed","count":p_count})
|
| 1644 |
+
if ownership == "study" or method_scope_unknown:
|
| 1645 |
+
method_frame = cls._first_performed_unowned_quantitative_term(revised, forbidden)
|
| 1646 |
+
if method_frame:
|
| 1647 |
+
diagnostics.append({
|
| 1648 |
+
"type":"unsupported_quantitative_method_removed",
|
| 1649 |
+
"method":method_frame.get("term"),
|
| 1650 |
+
"claim_state":method_frame.get("state"),
|
| 1651 |
+
"method_category":method_frame.get("category"),
|
| 1652 |
+
"preview":sentence.strip()[:180],
|
| 1653 |
+
})
|
| 1654 |
revised = ""
|
| 1655 |
|
| 1656 |
def replace_local_metric(pattern, expected, *, dtype="float", label="metric"):
|