Spaces:
Sleeping
Sleeping
P3-A4 citation authority repair
#151
by GuXiaopeng - opened
- core/generation_runner.py +0 -0
- core/pipeline_agent.py +284 -322
core/generation_runner.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
core/pipeline_agent.py
CHANGED
|
@@ -1640,117 +1640,6 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1640 |
return cleaned
|
| 1641 |
|
| 1642 |
|
| 1643 |
-
@staticmethod
|
| 1644 |
-
def _english_reader_surface_has_foreign_script(value):
|
| 1645 |
-
"""Return True only for scripts that cannot remain in an English reader-facing surface.
|
| 1646 |
-
|
| 1647 |
-
User intent may be entered in Chinese, Russian, or Kazakh, but the English
|
| 1648 |
-
manuscript projection must not leak Han or Cyrillic source text. Latin
|
| 1649 |
-
acronyms, numerals, punctuation, and ordinary English remain untouched.
|
| 1650 |
-
"""
|
| 1651 |
-
return bool(re.search(r'[\u3400-\u9fff\u3040-\u30ff\u0400-\u052f]', str(value or '')))
|
| 1652 |
-
|
| 1653 |
-
@classmethod
|
| 1654 |
-
def validate_reader_facing_english_projection(cls, projection, *, expected_keyword_count=None):
|
| 1655 |
-
"""Validate a language projection without changing the user's semantic authority."""
|
| 1656 |
-
record = dict(projection or {})
|
| 1657 |
-
title = str(record.get('title') or record.get('english_title') or '').strip()
|
| 1658 |
-
raw_keywords = record.get('keywords')
|
| 1659 |
-
if raw_keywords is None:
|
| 1660 |
-
raw_keywords = record.get('english_keywords')
|
| 1661 |
-
keywords = [str(item).strip() for item in (raw_keywords or []) if str(item).strip()]
|
| 1662 |
-
if not title:
|
| 1663 |
-
raise DeepSeekRequestError('English reader-facing title is empty.')
|
| 1664 |
-
if expected_keyword_count is not None and len(keywords) != int(expected_keyword_count):
|
| 1665 |
-
raise DeepSeekRequestError(
|
| 1666 |
-
f'English reader-facing keyword cardinality changed: expected={int(expected_keyword_count)} observed={len(keywords)}.'
|
| 1667 |
-
)
|
| 1668 |
-
combined = ' '.join([title] + keywords)
|
| 1669 |
-
if cls._english_reader_surface_has_foreign_script(combined):
|
| 1670 |
-
raise DeepSeekRequestError('English reader-facing title/keywords contain untranslated Han or Cyrillic source text.')
|
| 1671 |
-
return {'title': title, 'keywords': keywords}
|
| 1672 |
-
|
| 1673 |
-
def normalize_reader_facing_front_matter_to_english(self, topic, keywords):
|
| 1674 |
-
"""Create a faithful English language projection of user Title/Keywords.
|
| 1675 |
-
|
| 1676 |
-
The original user strings remain semantic authority in the study contract.
|
| 1677 |
-
This method is presentation-only: English input is passed through without a
|
| 1678 |
-
model call; non-English/mixed-script input is translated, never optimized.
|
| 1679 |
-
Keyword cardinality and order are invariant.
|
| 1680 |
-
"""
|
| 1681 |
-
source_title = str(topic or '').strip()
|
| 1682 |
-
source_keywords = parse_keyword_terms(keywords, explicit_user=True)
|
| 1683 |
-
if not source_title:
|
| 1684 |
-
raise DeepSeekRequestError('User research title is empty before English language projection.')
|
| 1685 |
-
|
| 1686 |
-
source_surface = ' '.join([source_title] + source_keywords)
|
| 1687 |
-
if not self._english_reader_surface_has_foreign_script(source_surface):
|
| 1688 |
-
return {
|
| 1689 |
-
'title': source_title,
|
| 1690 |
-
'keywords': list(source_keywords),
|
| 1691 |
-
'translation_applied': False,
|
| 1692 |
-
}
|
| 1693 |
-
|
| 1694 |
-
indexed_keywords = [
|
| 1695 |
-
{'index': index, 'text': value}
|
| 1696 |
-
for index, value in enumerate(source_keywords, start=1)
|
| 1697 |
-
]
|
| 1698 |
-
payload = {'title': source_title, 'keywords': indexed_keywords}
|
| 1699 |
-
base_prompt = f"""Translate ONLY the supplied user research Title and Keywords into academic English.
|
| 1700 |
-
Return JSON only: {{"title":"...","keywords":["..."]}}.
|
| 1701 |
-
|
| 1702 |
-
LANGUAGE-AUTHORITY CONTRACT:
|
| 1703 |
-
1. This is translation, not title optimization, rewriting, topic expansion, or keyword generation.
|
| 1704 |
-
2. Preserve the complete research meaning: focal constructs, direction/relationship wording, mediator/moderator roles, population/context, method qualifiers, acronyms, numbers, and scope.
|
| 1705 |
-
3. Do not add, delete, merge, split, reorder, generalize, specialize, or reinterpret any keyword. Return exactly {len(source_keywords)} keywords in the supplied order.
|
| 1706 |
-
4. Preserve existing English technical identities and acronyms. Translate/transliterate Han or Cyrillic reader-facing text so the returned Title/Keywords contain no Chinese, Russian, or Kazakh script.
|
| 1707 |
-
5. Do not mention the translation process and do not wrap JSON in Markdown.
|
| 1708 |
-
|
| 1709 |
-
<SOURCE_USER_FRONT_MATTER>
|
| 1710 |
-
{json.dumps(payload, ensure_ascii=False)}
|
| 1711 |
-
</SOURCE_USER_FRONT_MATTER>"""
|
| 1712 |
-
current_prompt = base_prompt
|
| 1713 |
-
last_error = None
|
| 1714 |
-
for local_attempt in range(2):
|
| 1715 |
-
response = self.call_deepseek(
|
| 1716 |
-
'You are a precise academic translator. Preserve research semantics exactly and change only language surface.',
|
| 1717 |
-
current_prompt,
|
| 1718 |
-
max_tokens=max(700, min(1600, 500 + len(source_keywords) * 90)),
|
| 1719 |
-
temp=0.0,
|
| 1720 |
-
model_role='writing',
|
| 1721 |
-
thinking=False,
|
| 1722 |
-
allow_fallback=False,
|
| 1723 |
-
response_format={'type': 'json_object'},
|
| 1724 |
-
logical_node='front_matter_writing',
|
| 1725 |
-
)
|
| 1726 |
-
try:
|
| 1727 |
-
parsed = self._extract_json_from_text(response)
|
| 1728 |
-
if not isinstance(parsed, dict):
|
| 1729 |
-
raise DeepSeekRequestError('English front-matter language projection was not valid JSON.')
|
| 1730 |
-
candidate = self.validate_reader_facing_english_projection(
|
| 1731 |
-
parsed, expected_keyword_count=len(source_keywords)
|
| 1732 |
-
)
|
| 1733 |
-
return {
|
| 1734 |
-
'title': candidate['title'],
|
| 1735 |
-
'keywords': candidate['keywords'],
|
| 1736 |
-
'translation_applied': True,
|
| 1737 |
-
}
|
| 1738 |
-
except DeepSeekRequestError as exc:
|
| 1739 |
-
last_error = exc
|
| 1740 |
-
if local_attempt >= 1:
|
| 1741 |
-
raise
|
| 1742 |
-
logging.warning(
|
| 1743 |
-
'English front-matter language projection retry | attempt=%s/2 | reason=%s',
|
| 1744 |
-
local_attempt + 1, self._safe_source_error(exc),
|
| 1745 |
-
)
|
| 1746 |
-
current_prompt = base_prompt + (
|
| 1747 |
-
'\n\nLOCAL VALIDATION RETRY: The prior response violated the language/cardinality contract. '
|
| 1748 |
-
'Return a fresh complete JSON object only. Translate the same Title and the same indexed Keywords '
|
| 1749 |
-
'faithfully into English, preserve keyword order/count exactly, and remove all Han/Cyrillic source text. '
|
| 1750 |
-
'Do not optimize or rewrite the research intent.'
|
| 1751 |
-
)
|
| 1752 |
-
raise DeepSeekRequestError(f'English front-matter language projection failed: {last_error}')
|
| 1753 |
-
|
| 1754 |
@staticmethod
|
| 1755 |
def _front_matter_has_citation(value):
|
| 1756 |
text = str(value or '')
|
|
@@ -1790,8 +1679,6 @@ LANGUAGE-AUTHORITY CONTRACT:
|
|
| 1790 |
word_count = cls._working_abstract_word_count(text)
|
| 1791 |
if cls._front_matter_has_citation(text):
|
| 1792 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
| 1793 |
-
if cls._english_reader_surface_has_foreign_script(text):
|
| 1794 |
-
raise DeepSeekRequestError('Working Abstract contains untranslated Han or Cyrillic source text.')
|
| 1795 |
forbidden = (
|
| 1796 |
('RESULT_ASSERTION', r'\bwe\s+(?:find|found|show|demonstrate|observe|establish)\b'),
|
| 1797 |
('RESULT_ASSERTION', r'\b(?:results?|findings?)\s+(?:show|indicate|reveal|demonstrate|confirm|support)\b'),
|
|
@@ -1988,28 +1875,24 @@ LANGUAGE-AUTHORITY CONTRACT:
|
|
| 1988 |
return issues
|
| 1989 |
|
| 1990 |
|
| 1991 |
-
def reconcile_quantitative_reader_front_matter(
|
| 1992 |
-
|
| 1993 |
-
):
|
| 1994 |
-
"""Preserve source semantic authority while allowing a faithful English surface.
|
| 1995 |
|
| 1996 |
-
``user_research_intent``
|
| 1997 |
-
|
| 1998 |
-
|
| 1999 |
-
|
|
|
|
| 2000 |
"""
|
| 2001 |
record = dict(contract or {})
|
| 2002 |
intent = dict(record.get("user_research_intent") or {})
|
| 2003 |
policy = keyword_policy_from_contract(record, fallback_keywords=keywords)
|
| 2004 |
source_topic = str(intent.get("source_topic") or record.get("source_topic") or topic or "").strip()
|
| 2005 |
source_keywords = list(intent.get("source_keywords") or [])
|
| 2006 |
-
reader_source_topic = str(reader_title or source_topic or topic or "").strip()
|
| 2007 |
-
reader_source_keywords = parse_keyword_terms(reader_keywords, explicit_user=True) if reader_keywords is not None else list(source_keywords)
|
| 2008 |
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and source_keywords:
|
| 2009 |
-
authoritative_reader = reader_source_keywords or source_keywords
|
| 2010 |
return {
|
| 2011 |
-
"title":
|
| 2012 |
-
"keywords": apply_keyword_policy(
|
| 2013 |
}
|
| 2014 |
|
| 2015 |
# No explicit user keyword authority: retain user-authoritative title when
|
|
@@ -2027,7 +1910,7 @@ LANGUAGE-AUTHORITY CONTRACT:
|
|
| 2027 |
logging.warning("Front-matter semantic contract missing frozen constructs; using degraded fallback.")
|
| 2028 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2029 |
if intent and source_topic:
|
| 2030 |
-
fallback["title"] =
|
| 2031 |
return fallback
|
| 2032 |
|
| 2033 |
source_keywords = list(record.get("source_keywords") or [])
|
|
@@ -2116,7 +1999,7 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 2116 |
extra = [str(item).strip() for item in ((critic or {}).get("unmeasured_focal_constructs") or []) if str(item).strip()]
|
| 2117 |
missing = [str(item).strip() for item in ((critic or {}).get("missing_frozen_constructs") or []) if str(item).strip()]
|
| 2118 |
if not invalid and not extra and not missing:
|
| 2119 |
-
resolved_title =
|
| 2120 |
return {
|
| 2121 |
"title": resolved_title,
|
| 2122 |
"keywords": apply_keyword_policy(candidate.get("keywords") or [], policy),
|
|
@@ -2130,11 +2013,7 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 2130 |
|
| 2131 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2132 |
if intent and source_topic:
|
| 2133 |
-
fallback["title"] =
|
| 2134 |
-
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and reader_source_keywords:
|
| 2135 |
-
fallback["keywords"] = apply_keyword_policy(
|
| 2136 |
-
reader_source_keywords, policy, authoritative=reader_source_keywords
|
| 2137 |
-
)
|
| 2138 |
logging.warning("Adaptive front-matter reconciliation exhausted; using deterministic degraded fallback instead of failing manuscript generation.")
|
| 2139 |
return fallback
|
| 2140 |
|
|
@@ -2149,7 +2028,7 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 2149 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 2150 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2151 |
Aim for approximately 120-180 English words, have no citations, and function only as a continuity anchor. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor over mechanical compliance. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 2152 |
-
Preserve the
|
| 2153 |
TOPIC: {topic}
|
| 2154 |
USER KEYWORDS: {keywords}
|
| 2155 |
<PROVISIONAL_RESEARCH_LOGIC>
|
|
@@ -2186,7 +2065,7 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2186 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 2187 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2188 |
Aim for a concise Abstract of roughly 120-200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 2189 |
-
Preserve the
|
| 2190 |
TOPIC: {topic}
|
| 2191 |
USER KEYWORDS: {keywords}
|
| 2192 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
@@ -2206,8 +2085,6 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2206 |
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
|
| 2207 |
if self._front_matter_has_citation(abstract):
|
| 2208 |
raise DeepSeekRequestError('Final Abstract must not contain citations.')
|
| 2209 |
-
if self._english_reader_surface_has_foreign_script(abstract):
|
| 2210 |
-
raise DeepSeekRequestError('Final Abstract contains untranslated Han or Cyrillic source text.')
|
| 2211 |
meta_findings = self.inspect_authorial_ai_meta(abstract)
|
| 2212 |
if meta_findings:
|
| 2213 |
raise DeepSeekRequestError('Final Abstract contains high-confidence authorial AI-writing meta-commentary.')
|
|
@@ -3054,17 +2931,8 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 3054 |
future_lane[executor.submit(self._s2_worker, query, 0)] = 'method_authority'
|
| 3055 |
future_lane[executor.submit(self._oa_worker, query, 1, "language:en,type:article,is_paratext:false")] = 'method_authority'
|
| 3056 |
for future in concurrent.futures.as_completed(future_lane):
|
|
|
|
| 3057 |
lane = future_lane[future]
|
| 3058 |
-
try:
|
| 3059 |
-
results = future.result()
|
| 3060 |
-
except PipelineCancelledError:
|
| 3061 |
-
raise
|
| 3062 |
-
except Exception as exc:
|
| 3063 |
-
logging.warning(
|
| 3064 |
-
"Delivery-first METHOD_AUTHORITY source skipped | lane=%s error=%s",
|
| 3065 |
-
lane, type(exc).__name__,
|
| 3066 |
-
)
|
| 3067 |
-
continue
|
| 3068 |
with self.lock:
|
| 3069 |
for res in results:
|
| 3070 |
result_keys = self._reference_keys(res)
|
|
@@ -3098,44 +2966,28 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 3098 |
sys_prompt = """You are an expert academic librarian. Generate exactly 3 concise English search queries.
|
| 3099 |
[CRITICAL SEARCH QUERY RULES]
|
| 3100 |
1. DO NOT use complex, deeply nested boolean logic (e.g., Avoid `(A OR B) AND (C OR D)`).
|
| 3101 |
-
2. DO NOT use excessive quotation marks for long phrases.
|
| 3102 |
-
3. Output simple, space-separated keyword combinations or simple AND logic.
|
| 3103 |
Good: "educational chatbot" "recommendation system" context-aware
|
| 3104 |
Bad: ("educational chatbot" OR "pedagogical agent") AND ("recommendation system")
|
| 3105 |
4. Keep the query strictly UNDER 5 terms to avoid triggering server API timeouts.
|
| 3106 |
Return ONLY a valid JSON array of strings."""
|
| 3107 |
user_prompt = f"Topic: {topic}\nKeywords: {keywords}"
|
| 3108 |
for attempt in range(3):
|
| 3109 |
-
|
| 3110 |
-
|
| 3111 |
-
|
| 3112 |
-
|
| 3113 |
-
|
| 3114 |
-
|
| 3115 |
-
|
| 3116 |
-
))
|
| 3117 |
-
except PipelineCancelledError:
|
| 3118 |
-
raise
|
| 3119 |
-
except Exception as exc:
|
| 3120 |
-
logging.warning(
|
| 3121 |
-
"Delivery-first search-query generation fallback | attempt=%s/3 error=%s",
|
| 3122 |
-
attempt + 1, type(exc).__name__,
|
| 3123 |
-
)
|
| 3124 |
-
queries = []
|
| 3125 |
if queries and isinstance(queries, list) and len(queries) > 0:
|
| 3126 |
final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
|
| 3127 |
-
if final_queries:
|
| 3128 |
return final_queries[:3]
|
| 3129 |
time.sleep(1)
|
| 3130 |
-
|
| 3131 |
-
clean_chunks = [
|
| 3132 |
-
c.strip() for c in re.findall(r'[a-zA-Z0-9\s\-]{4,}', f"{topic} {keywords}")
|
| 3133 |
-
if self._is_english_dominant(c.strip())
|
| 3134 |
-
]
|
| 3135 |
-
if clean_chunks:
|
| 3136 |
-
logging.warning(
|
| 3137 |
-
"Delivery-first search-query provider exhausted; using deterministic English user-text query fallback."
|
| 3138 |
-
)
|
| 3139 |
return clean_chunks[:3] if clean_chunks else []
|
| 3140 |
|
| 3141 |
@staticmethod
|
|
@@ -3566,18 +3418,7 @@ Return ONLY a valid JSON array of strings."""
|
|
| 3566 |
for future in concurrent.futures.as_completed(future_sources):
|
| 3567 |
self._check_cancelled()
|
| 3568 |
source = future_sources[future]
|
| 3569 |
-
|
| 3570 |
-
rows = future.result()
|
| 3571 |
-
except PipelineCancelledError:
|
| 3572 |
-
raise
|
| 3573 |
-
except Exception as exc:
|
| 3574 |
-
self._record_source_error(source, 'RETRIEVAL_FUTURE_ERROR', detail=exc)
|
| 3575 |
-
logging.warning(
|
| 3576 |
-
"Delivery-first literature source future skipped | source=%s error=%s",
|
| 3577 |
-
source, type(exc).__name__,
|
| 3578 |
-
)
|
| 3579 |
-
rows = []
|
| 3580 |
-
collected[source].extend(rows)
|
| 3581 |
if progress_callback:
|
| 3582 |
progress_callback()
|
| 3583 |
|
|
@@ -3586,24 +3427,15 @@ Return ONLY a valid JSON array of strings."""
|
|
| 3586 |
if phase == 0:
|
| 3587 |
terms = self.generate_search_queries(topic, keywords)
|
| 3588 |
else:
|
| 3589 |
-
|
| 3590 |
-
|
| 3591 |
-
|
| 3592 |
-
|
| 3593 |
-
|
| 3594 |
-
|
| 3595 |
-
|
| 3596 |
-
|
| 3597 |
-
|
| 3598 |
-
terms = parsed if isinstance(parsed, list) else []
|
| 3599 |
-
except PipelineCancelledError:
|
| 3600 |
-
raise
|
| 3601 |
-
except Exception as exc:
|
| 3602 |
-
logging.warning(
|
| 3603 |
-
'Delivery-first auxiliary search-query phase skipped | phase=%s error=%s',
|
| 3604 |
-
phase, type(exc).__name__,
|
| 3605 |
-
)
|
| 3606 |
-
terms = []
|
| 3607 |
|
| 3608 |
normalized_terms = []
|
| 3609 |
for value in terms:
|
|
@@ -5256,84 +5088,6 @@ INPUT:
|
|
| 5256 |
return True
|
| 5257 |
return False
|
| 5258 |
|
| 5259 |
-
@staticmethod
|
| 5260 |
-
def _has_explicit_current_qualitative_study_anchor(sentence):
|
| 5261 |
-
"""Identify an explicit current-study owner even in a cited sentence.
|
| 5262 |
-
|
| 5263 |
-
A method citation may justify a procedure, but it must not make this
|
| 5264 |
-
manuscript's own empirical/procedural clause externally owned. Keep
|
| 5265 |
-
this narrower than the general qualitative ownership heuristic so a
|
| 5266 |
-
cited phrase such as ``the study conducted interviews`` is not
|
| 5267 |
-
accidentally reclassified as the current manuscript.
|
| 5268 |
-
"""
|
| 5269 |
-
value = str(sentence or "")
|
| 5270 |
-
return bool(re.search(
|
| 5271 |
-
r"(?i)\b(?:we|our\s+study|this\s+study|the\s+present\s+study|"
|
| 5272 |
-
r"research\s+team|our\s+data|our\s+analysis|our\s+interviews?|"
|
| 5273 |
-
r"our\s+focus\s+groups?|our\s+observations?|in\s+this\s+study|"
|
| 5274 |
-
r"in\s+our\s+study)\b",
|
| 5275 |
-
value,
|
| 5276 |
-
))
|
| 5277 |
-
|
| 5278 |
-
@staticmethod
|
| 5279 |
-
def _normalize_qualitative_analytic_design_identity(value):
|
| 5280 |
-
folded = re.sub(r"\s+", " ", str(value or "")).strip().casefold()
|
| 5281 |
-
if not folded:
|
| 5282 |
-
return ""
|
| 5283 |
-
if "gioia" in folded:
|
| 5284 |
-
return "gioia"
|
| 5285 |
-
if "grounded theory" in folded:
|
| 5286 |
-
return "grounded_theory"
|
| 5287 |
-
if "comparative case" in folded:
|
| 5288 |
-
return "comparative_case"
|
| 5289 |
-
if "longitudinal" in folded and "qualitative" in folded:
|
| 5290 |
-
return "longitudinal_qualitative"
|
| 5291 |
-
if "thematic" in folded:
|
| 5292 |
-
return "thematic_process"
|
| 5293 |
-
if "process analysis" in folded:
|
| 5294 |
-
return "process_analysis"
|
| 5295 |
-
return folded
|
| 5296 |
-
|
| 5297 |
-
@classmethod
|
| 5298 |
-
def _performed_qualitative_analytic_design_identity(cls, sentence):
|
| 5299 |
-
"""Return a performed current-study qualitative design, if explicit.
|
| 5300 |
-
|
| 5301 |
-
This is a prose-level evidence guard, not a new method router. It
|
| 5302 |
-
prevents a cited method name from silently replacing the analytic design
|
| 5303 |
-
frozen in the qualitative evidence packet.
|
| 5304 |
-
"""
|
| 5305 |
-
value = re.sub(r"\s+", " ", str(sentence or "")).strip()
|
| 5306 |
-
if not value or not cls._has_explicit_current_qualitative_study_anchor(value):
|
| 5307 |
-
return ""
|
| 5308 |
-
candidates = (
|
| 5309 |
-
("gioia", r"\bGioia(?:-style)?(?:\s+inductive\s+coding|\s+methodology|\s+method|\s+approach)?\b"),
|
| 5310 |
-
("grounded_theory", r"\bgrounded\s+theory\b"),
|
| 5311 |
-
("comparative_case", r"\bcomparative\s+case\s+analysis\b"),
|
| 5312 |
-
("longitudinal_qualitative", r"\blongitudinal\s+qualitative\s+analysis\b"),
|
| 5313 |
-
("process_analysis", r"\bprocess\s+analysis\b"),
|
| 5314 |
-
("thematic_process", r"\b(?:iterative\s+)?thematic(?:/process)?\s+analysis\b|\bthematic\s+analysis\b"),
|
| 5315 |
-
)
|
| 5316 |
-
for identity, pattern in candidates:
|
| 5317 |
-
match = re.search(pattern, value, flags=re.I)
|
| 5318 |
-
if not match:
|
| 5319 |
-
continue
|
| 5320 |
-
before = value[max(0, match.start() - 120):match.start()]
|
| 5321 |
-
if re.search(
|
| 5322 |
-
r"(?i)\b(?:did\s+not|does\s+not|do\s+not|was\s+not|were\s+not|"
|
| 5323 |
-
r"not|without|never)\b[^.;:]{0,45}$",
|
| 5324 |
-
before,
|
| 5325 |
-
):
|
| 5326 |
-
continue
|
| 5327 |
-
full_before = value[:match.start()]
|
| 5328 |
-
if re.search(
|
| 5329 |
-
r"(?i)\b(?:use|uses|used|employ|employs|employed|apply|applies|applied|"
|
| 5330 |
-
r"adopt|adopts|adopted|follow|follows|followed|conduct|conducts|conducted|"
|
| 5331 |
-
r"analy[sz]e|analy[sz]es|analy[sz]ed|code|codes|coded)\b[^.;:]{0,180}$",
|
| 5332 |
-
full_before,
|
| 5333 |
-
):
|
| 5334 |
-
return identity
|
| 5335 |
-
return ""
|
| 5336 |
-
|
| 5337 |
@classmethod
|
| 5338 |
def _is_study_owned_qualitative_claim(cls, sentence, section_name=""):
|
| 5339 |
"""Conservatively identify only this manuscript's empirical/procedural claims.
|
|
@@ -5348,11 +5102,6 @@ INPUT:
|
|
| 5348 |
return False
|
| 5349 |
if re.match(r'^["“‘\']', stripped) or re.search(r"\bQUOTE-\d+\b", value, flags=re.I):
|
| 5350 |
return False
|
| 5351 |
-
# Explicit current-study ownership dominates an appended method or
|
| 5352 |
-
# theory citation. The cited source may justify the procedure, but it
|
| 5353 |
-
# cannot own the statement that *this* study performed it.
|
| 5354 |
-
if cls._has_explicit_current_qualitative_study_anchor(value):
|
| 5355 |
-
return True
|
| 5356 |
if cls._has_external_literature_marker(value):
|
| 5357 |
return False
|
| 5358 |
if re.search(r"(?i)\b(?:prior|previous|earlier|existing)\s+(?:research|stud(?:y|ies)|literature)\b", value):
|
|
@@ -5406,9 +5155,6 @@ INPUT:
|
|
| 5406 |
"ethics": [str(x or "").strip().casefold() for x in (facts.get("ethics_procedures") or []) if str(x or "").strip()],
|
| 5407 |
"reflexivity": [str(x or "").strip().casefold() for x in (facts.get("reflexivity_procedures") or []) if str(x or "").strip()],
|
| 5408 |
}
|
| 5409 |
-
authorized_design = cls._normalize_qualitative_analytic_design_identity(
|
| 5410 |
-
facts.get("analytic_design") or facts.get("analysis_method")
|
| 5411 |
-
)
|
| 5412 |
procedure_patterns = {
|
| 5413 |
"trustworthiness": r"\bmember\s+check(?:ing|s)?\b|\bpeer\s+debrief(?:ing|ed)?\b|\baudit\s+trail\b|\bexternal\s+audit\b",
|
| 5414 |
"ethics": r"\binformed\s+consent\b|\bethics?\s+approval\b|\binstitutional\s+review\s+board\b|\bIRB\b|\bapproval\s+(?:number|code)\b",
|
|
@@ -5445,18 +5191,6 @@ INPUT:
|
|
| 5445 |
if not core.strip() or not cls._is_study_owned_qualitative_claim(core, section_name):
|
| 5446 |
continue
|
| 5447 |
|
| 5448 |
-
observed_design = cls._performed_qualitative_analytic_design_identity(core)
|
| 5449 |
-
if observed_design and authorized_design and observed_design != authorized_design:
|
| 5450 |
-
sig = ("analytic_design", observed_design)
|
| 5451 |
-
if sig not in seen:
|
| 5452 |
-
seen.add(sig)
|
| 5453 |
-
violations.append({
|
| 5454 |
-
"type":"unsupported_qualitative_analytic_design",
|
| 5455 |
-
"observed":observed_design,
|
| 5456 |
-
"expected":authorized_design,
|
| 5457 |
-
"preview":core.strip()[:180],
|
| 5458 |
-
})
|
| 5459 |
-
|
| 5460 |
# Check every explicit procedure mention; do not stop after the first.
|
| 5461 |
for category, pattern in procedure_patterns.items():
|
| 5462 |
for match in re.finditer(pattern, core, flags=re.I):
|
|
@@ -5519,9 +5253,6 @@ INPUT:
|
|
| 5519 |
"ethics": [str(x or "").strip() for x in (facts.get("ethics_procedures") or []) if str(x or "").strip()],
|
| 5520 |
"reflexivity": [str(x or "").strip() for x in (facts.get("reflexivity_procedures") or []) if str(x or "").strip()],
|
| 5521 |
}
|
| 5522 |
-
authorized_design = cls._normalize_qualitative_analytic_design_identity(
|
| 5523 |
-
facts.get("analytic_design") or facts.get("analysis_method")
|
| 5524 |
-
)
|
| 5525 |
allowed_by_actor = {}
|
| 5526 |
canonical_relations = []
|
| 5527 |
canonical_by_actor = {}
|
|
@@ -5556,16 +5287,6 @@ INPUT:
|
|
| 5556 |
lower = core.casefold()
|
| 5557 |
if not core.strip() or not cls._is_study_owned_qualitative_claim(core, section_name):
|
| 5558 |
continue
|
| 5559 |
-
observed_design = cls._performed_qualitative_analytic_design_identity(core)
|
| 5560 |
-
if observed_design and authorized_design and observed_design != authorized_design:
|
| 5561 |
-
replacements.append((start, end, ""))
|
| 5562 |
-
diagnostics.append({
|
| 5563 |
-
"type":"unsupported_qualitative_analytic_design_removed",
|
| 5564 |
-
"observed":observed_design,
|
| 5565 |
-
"expected":authorized_design,
|
| 5566 |
-
"preview":core.strip()[:180],
|
| 5567 |
-
})
|
| 5568 |
-
continue
|
| 5569 |
removed = False
|
| 5570 |
for category, pattern in procedure_patterns.items():
|
| 5571 |
procedure_match = re.search(pattern, core, flags=re.I)
|
|
@@ -6349,6 +6070,199 @@ Return exactly:
|
|
| 6349 |
)
|
| 6350 |
return revised
|
| 6351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6352 |
def audit_and_resolve_citations(
|
| 6353 |
self, text, section_name='', section_id='', quarantine=False, allowed_tags=None
|
| 6354 |
):
|
|
@@ -6358,12 +6272,41 @@ Return exactly:
|
|
| 6358 |
)
|
| 6359 |
value = self.strip_ineligible_current_study_citations(str(text or ''), section_name)
|
| 6360 |
if quarantine:
|
| 6361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6362 |
unknown = sorted({f"[{tag.upper()}]" for tag in re.findall(r'CITE-\d+', value, re.I)} - set(self.citation_map))
|
| 6363 |
if unknown:
|
| 6364 |
for tag in unknown:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6365 |
value = re.sub(rf'\s*\[?{re.escape(tag.strip("[]"))}\]?\s*', ' ', value, flags=re.I)
|
| 6366 |
-
self._append_citation_audit(section_name, '
|
|
|
|
| 6367 |
allowed = None if allowed_tags is None else {
|
| 6368 |
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 6369 |
}
|
|
@@ -6383,6 +6326,16 @@ Return exactly:
|
|
| 6383 |
self._append_citation_audit(
|
| 6384 |
section_name, 'reject_outside_frozen_section_whitelist', text, value, tag
|
| 6385 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6386 |
has_citations = bool(re.search(r'CITE-\d+', value, re.I))
|
| 6387 |
violations = self.find_multi_citation_violations(value)
|
| 6388 |
if has_citations and self.citation_semantic_audit.get('enabled', True):
|
|
@@ -6413,12 +6366,21 @@ Return exactly:
|
|
| 6413 |
value = self._local_citation_recovery(value, section_name, retain_lexical=False)
|
| 6414 |
elif violations:
|
| 6415 |
value = self._local_citation_recovery(value, section_name)
|
| 6416 |
-
|
|
|
|
|
|
|
| 6417 |
self._last_citation_semantic_violations = (
|
| 6418 |
-
list(
|
| 6419 |
-
+ list(self._last_citation_semantic_violations or [])
|
| 6420 |
-
+ list(outside_rows)
|
| 6421 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6422 |
if self.find_multi_citation_violations(value):
|
| 6423 |
value = self._local_citation_recovery(value, section_name)
|
| 6424 |
if self.find_multi_citation_violations(value):
|
|
|
|
| 1640 |
return cleaned
|
| 1641 |
|
| 1642 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1643 |
@staticmethod
|
| 1644 |
def _front_matter_has_citation(value):
|
| 1645 |
text = str(value or '')
|
|
|
|
| 1679 |
word_count = cls._working_abstract_word_count(text)
|
| 1680 |
if cls._front_matter_has_citation(text):
|
| 1681 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
|
|
|
|
|
|
| 1682 |
forbidden = (
|
| 1683 |
('RESULT_ASSERTION', r'\bwe\s+(?:find|found|show|demonstrate|observe|establish)\b'),
|
| 1684 |
('RESULT_ASSERTION', r'\b(?:results?|findings?)\s+(?:show|indicate|reveal|demonstrate|confirm|support)\b'),
|
|
|
|
| 1875 |
return issues
|
| 1876 |
|
| 1877 |
|
| 1878 |
+
def reconcile_quantitative_reader_front_matter(self, *, topic, keywords, contract, manuscript=""):
|
| 1879 |
+
"""Preserve R5 user intent while retaining legacy contract reconciliation.
|
|
|
|
|
|
|
| 1880 |
|
| 1881 |
+
Contracts that carry ``user_research_intent`` are authoritative: the
|
| 1882 |
+
finalizer must not repair an upstream design mismatch by silently
|
| 1883 |
+
renaming the user study. Older contracts without that explicit R5
|
| 1884 |
+
authority retain the pre-R5 bounded reconciliation path so historical
|
| 1885 |
+
checkpoints and compatibility behavior remain stable.
|
| 1886 |
"""
|
| 1887 |
record = dict(contract or {})
|
| 1888 |
intent = dict(record.get("user_research_intent") or {})
|
| 1889 |
policy = keyword_policy_from_contract(record, fallback_keywords=keywords)
|
| 1890 |
source_topic = str(intent.get("source_topic") or record.get("source_topic") or topic or "").strip()
|
| 1891 |
source_keywords = list(intent.get("source_keywords") or [])
|
|
|
|
|
|
|
| 1892 |
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and source_keywords:
|
|
|
|
| 1893 |
return {
|
| 1894 |
+
"title": source_topic,
|
| 1895 |
+
"keywords": apply_keyword_policy(source_keywords, policy, authoritative=source_keywords),
|
| 1896 |
}
|
| 1897 |
|
| 1898 |
# No explicit user keyword authority: retain user-authoritative title when
|
|
|
|
| 1910 |
logging.warning("Front-matter semantic contract missing frozen constructs; using degraded fallback.")
|
| 1911 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 1912 |
if intent and source_topic:
|
| 1913 |
+
fallback["title"] = source_topic
|
| 1914 |
return fallback
|
| 1915 |
|
| 1916 |
source_keywords = list(record.get("source_keywords") or [])
|
|
|
|
| 1999 |
extra = [str(item).strip() for item in ((critic or {}).get("unmeasured_focal_constructs") or []) if str(item).strip()]
|
| 2000 |
missing = [str(item).strip() for item in ((critic or {}).get("missing_frozen_constructs") or []) if str(item).strip()]
|
| 2001 |
if not invalid and not extra and not missing:
|
| 2002 |
+
resolved_title = source_topic if intent else re.sub(r"\s+", " ", str(candidate.get("title") or "")).strip()
|
| 2003 |
return {
|
| 2004 |
"title": resolved_title,
|
| 2005 |
"keywords": apply_keyword_policy(candidate.get("keywords") or [], policy),
|
|
|
|
| 2013 |
|
| 2014 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2015 |
if intent and source_topic:
|
| 2016 |
+
fallback["title"] = source_topic
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2017 |
logging.warning("Adaptive front-matter reconciliation exhausted; using deterministic degraded fallback instead of failing manuscript generation.")
|
| 2018 |
return fallback
|
| 2019 |
|
|
|
|
| 2028 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 2029 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2030 |
Aim for approximately 120-180 English words, have no citations, and function only as a continuity anchor. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor over mechanical compliance. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 2031 |
+
Preserve the user keyword identities and research scope. Do not silently replace, delete, or reinterpret an explicit construct/relation because direct literature support is sparse. The user-supplied keyword list is authoritative for reader-facing keyword identity; wording changes belong only to target-language translation.
|
| 2032 |
TOPIC: {topic}
|
| 2033 |
USER KEYWORDS: {keywords}
|
| 2034 |
<PROVISIONAL_RESEARCH_LOGIC>
|
|
|
|
| 2065 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 2066 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2067 |
Aim for a concise Abstract of roughly 120-200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 2068 |
+
Preserve the user keyword identities and research scope. Do not silently replace, delete, or reinterpret an explicit construct/relation because direct literature support is sparse. The user-supplied keyword list is authoritative for reader-facing keyword identity; wording changes belong only to target-language translation.
|
| 2069 |
TOPIC: {topic}
|
| 2070 |
USER KEYWORDS: {keywords}
|
| 2071 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
|
|
| 2085 |
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
|
| 2086 |
if self._front_matter_has_citation(abstract):
|
| 2087 |
raise DeepSeekRequestError('Final Abstract must not contain citations.')
|
|
|
|
|
|
|
| 2088 |
meta_findings = self.inspect_authorial_ai_meta(abstract)
|
| 2089 |
if meta_findings:
|
| 2090 |
raise DeepSeekRequestError('Final Abstract contains high-confidence authorial AI-writing meta-commentary.')
|
|
|
|
| 2931 |
future_lane[executor.submit(self._s2_worker, query, 0)] = 'method_authority'
|
| 2932 |
future_lane[executor.submit(self._oa_worker, query, 1, "language:en,type:article,is_paratext:false")] = 'method_authority'
|
| 2933 |
for future in concurrent.futures.as_completed(future_lane):
|
| 2934 |
+
results = future.result()
|
| 2935 |
lane = future_lane[future]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2936 |
with self.lock:
|
| 2937 |
for res in results:
|
| 2938 |
result_keys = self._reference_keys(res)
|
|
|
|
| 2966 |
sys_prompt = """You are an expert academic librarian. Generate exactly 3 concise English search queries.
|
| 2967 |
[CRITICAL SEARCH QUERY RULES]
|
| 2968 |
1. DO NOT use complex, deeply nested boolean logic (e.g., Avoid `(A OR B) AND (C OR D)`).
|
| 2969 |
+
2. DO NOT use excessive quotation marks for long phrases.
|
| 2970 |
+
3. Output simple, space-separated keyword combinations or simple AND logic.
|
| 2971 |
Good: "educational chatbot" "recommendation system" context-aware
|
| 2972 |
Bad: ("educational chatbot" OR "pedagogical agent") AND ("recommendation system")
|
| 2973 |
4. Keep the query strictly UNDER 5 terms to avoid triggering server API timeouts.
|
| 2974 |
Return ONLY a valid JSON array of strings."""
|
| 2975 |
user_prompt = f"Topic: {topic}\nKeywords: {keywords}"
|
| 2976 |
for attempt in range(3):
|
| 2977 |
+
queries = self._extract_json_from_text(self.call_deepseek(
|
| 2978 |
+
sys_prompt,
|
| 2979 |
+
user_prompt,
|
| 2980 |
+
temp=0.1 + attempt * 0.1,
|
| 2981 |
+
model_role="fast",
|
| 2982 |
+
logical_node='search_jit',
|
| 2983 |
+
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2984 |
if queries and isinstance(queries, list) and len(queries) > 0:
|
| 2985 |
final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
|
| 2986 |
+
if final_queries:
|
| 2987 |
return final_queries[:3]
|
| 2988 |
time.sleep(1)
|
| 2989 |
+
|
| 2990 |
+
clean_chunks = [c.strip() for c in re.findall(r'[a-zA-Z0-9\s\-]{4,}', f"{topic} {keywords}") if self._is_english_dominant(c.strip())]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2991 |
return clean_chunks[:3] if clean_chunks else []
|
| 2992 |
|
| 2993 |
@staticmethod
|
|
|
|
| 3418 |
for future in concurrent.futures.as_completed(future_sources):
|
| 3419 |
self._check_cancelled()
|
| 3420 |
source = future_sources[future]
|
| 3421 |
+
collected[source].extend(future.result())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3422 |
if progress_callback:
|
| 3423 |
progress_callback()
|
| 3424 |
|
|
|
|
| 3427 |
if phase == 0:
|
| 3428 |
terms = self.generate_search_queries(topic, keywords)
|
| 3429 |
else:
|
| 3430 |
+
response = self.call_deepseek(
|
| 3431 |
+
'You are an academic librarian. Return only a JSON array with three new concise English search queries, each under five terms.',
|
| 3432 |
+
f"Topic: {topic}\nKeywords: {keywords}\nUsed: {sorted(all_used_terms)}",
|
| 3433 |
+
temp=0.4,
|
| 3434 |
+
model_role='fast',
|
| 3435 |
+
logical_node='search_jit',
|
| 3436 |
+
)
|
| 3437 |
+
parsed = self._extract_json_from_text(response)
|
| 3438 |
+
terms = parsed if isinstance(parsed, list) else []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3439 |
|
| 3440 |
normalized_terms = []
|
| 3441 |
for value in terms:
|
|
|
|
| 5088 |
return True
|
| 5089 |
return False
|
| 5090 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5091 |
@classmethod
|
| 5092 |
def _is_study_owned_qualitative_claim(cls, sentence, section_name=""):
|
| 5093 |
"""Conservatively identify only this manuscript's empirical/procedural claims.
|
|
|
|
| 5102 |
return False
|
| 5103 |
if re.match(r'^["“‘\']', stripped) or re.search(r"\bQUOTE-\d+\b", value, flags=re.I):
|
| 5104 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5105 |
if cls._has_external_literature_marker(value):
|
| 5106 |
return False
|
| 5107 |
if re.search(r"(?i)\b(?:prior|previous|earlier|existing)\s+(?:research|stud(?:y|ies)|literature)\b", value):
|
|
|
|
| 5155 |
"ethics": [str(x or "").strip().casefold() for x in (facts.get("ethics_procedures") or []) if str(x or "").strip()],
|
| 5156 |
"reflexivity": [str(x or "").strip().casefold() for x in (facts.get("reflexivity_procedures") or []) if str(x or "").strip()],
|
| 5157 |
}
|
|
|
|
|
|
|
|
|
|
| 5158 |
procedure_patterns = {
|
| 5159 |
"trustworthiness": r"\bmember\s+check(?:ing|s)?\b|\bpeer\s+debrief(?:ing|ed)?\b|\baudit\s+trail\b|\bexternal\s+audit\b",
|
| 5160 |
"ethics": r"\binformed\s+consent\b|\bethics?\s+approval\b|\binstitutional\s+review\s+board\b|\bIRB\b|\bapproval\s+(?:number|code)\b",
|
|
|
|
| 5191 |
if not core.strip() or not cls._is_study_owned_qualitative_claim(core, section_name):
|
| 5192 |
continue
|
| 5193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5194 |
# Check every explicit procedure mention; do not stop after the first.
|
| 5195 |
for category, pattern in procedure_patterns.items():
|
| 5196 |
for match in re.finditer(pattern, core, flags=re.I):
|
|
|
|
| 5253 |
"ethics": [str(x or "").strip() for x in (facts.get("ethics_procedures") or []) if str(x or "").strip()],
|
| 5254 |
"reflexivity": [str(x or "").strip() for x in (facts.get("reflexivity_procedures") or []) if str(x or "").strip()],
|
| 5255 |
}
|
|
|
|
|
|
|
|
|
|
| 5256 |
allowed_by_actor = {}
|
| 5257 |
canonical_relations = []
|
| 5258 |
canonical_by_actor = {}
|
|
|
|
| 5287 |
lower = core.casefold()
|
| 5288 |
if not core.strip() or not cls._is_study_owned_qualitative_claim(core, section_name):
|
| 5289 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5290 |
removed = False
|
| 5291 |
for category, pattern in procedure_patterns.items():
|
| 5292 |
procedure_match = re.search(pattern, core, flags=re.I)
|
|
|
|
| 6070 |
)
|
| 6071 |
return revised
|
| 6072 |
|
| 6073 |
+
@staticmethod
|
| 6074 |
+
def _high_confidence_noncanonical_citation_spans(text):
|
| 6075 |
+
"""Return high-confidence direct author-year citation forms outside canonical parentheses.
|
| 6076 |
+
|
| 6077 |
+
These forms are never rebound heuristically. They are surfaced as typed
|
| 6078 |
+
section violations so the owning writer can retry with its frozen CITE
|
| 6079 |
+
capability or narrow the unsupported claim. Patterns intentionally avoid
|
| 6080 |
+
bare years and tokens containing digits to reduce false positives for
|
| 6081 |
+
model/version/statistical expressions.
|
| 6082 |
+
"""
|
| 6083 |
+
value = str(text or '')
|
| 6084 |
+
surname = r"[A-ZÀ-ÖØ-Þ][A-Za-zÀ-ÖØ-öø-ÿ'’.-]{1,39}"
|
| 6085 |
+
conjunction = r"(?:&|and|AND)"
|
| 6086 |
+
et_al = r"(?:et|Et|ET)\s+(?:al|Al|AL)\."
|
| 6087 |
+
author_group = rf"(?:{surname}(?:\s+{conjunction}\s+{surname})?|{surname}\s+{et_al})"
|
| 6088 |
+
year = r"(?:19|20)\d{2}[a-z]?"
|
| 6089 |
+
patterns = (
|
| 6090 |
+
re.compile(rf"\[(?P<cite>{author_group}\s*,\s*{year})\]"),
|
| 6091 |
+
re.compile(rf"(?<![\w\[])\b(?P<cite>{author_group})\s*\(\s*(?P<year>{year})\s*\)"),
|
| 6092 |
+
)
|
| 6093 |
+
spans = []
|
| 6094 |
+
for pattern in patterns:
|
| 6095 |
+
for match in pattern.finditer(value):
|
| 6096 |
+
candidate = match.group(0).strip()
|
| 6097 |
+
# Canonical SmartPaper markers are handled separately and always
|
| 6098 |
+
# contain the author and year inside the same parentheses.
|
| 6099 |
+
if candidate.startswith('(') and candidate.endswith(')'):
|
| 6100 |
+
continue
|
| 6101 |
+
spans.append((match.start(), match.end(), candidate))
|
| 6102 |
+
# Stable de-duplication for overlapping patterns.
|
| 6103 |
+
unique = []
|
| 6104 |
+
seen = set()
|
| 6105 |
+
for row in sorted(spans, key=lambda item: (item[0], item[1], item[2])):
|
| 6106 |
+
key = (row[0], row[1], row[2])
|
| 6107 |
+
if key not in seen:
|
| 6108 |
+
seen.add(key)
|
| 6109 |
+
unique.append(row)
|
| 6110 |
+
return unique
|
| 6111 |
+
|
| 6112 |
+
def _normalize_pre_resolved_citation_authority(self, text, *, allowed_tags=None, section_name=''):
|
| 6113 |
+
"""Normalize only exact, authoritative pre-resolved markers back to CITE tags.
|
| 6114 |
+
|
| 6115 |
+
The Section Writer is supposed to emit frozen ``[CITE-XX]`` capabilities.
|
| 6116 |
+
A model-emitted reader-facing author-year marker therefore has no provenance
|
| 6117 |
+
until it is matched exactly against the *compiled* citation authority.
|
| 6118 |
+
|
| 6119 |
+
Safe normalization requires all of the following:
|
| 6120 |
+
- exact marker equality after ``compile_citation_identities()``;
|
| 6121 |
+
- authoritative reference metadata;
|
| 6122 |
+
- one distinct source identity for the visible marker;
|
| 6123 |
+
- at least one matching tag inside this manuscript unit's whitelist.
|
| 6124 |
+
|
| 6125 |
+
Anything else becomes a typed violation. No fuzzy author/year matching,
|
| 6126 |
+
metadata synthesis, or silent citation deletion is permitted here.
|
| 6127 |
+
"""
|
| 6128 |
+
value = str(text or '')
|
| 6129 |
+
allowed = None if allowed_tags is None else {
|
| 6130 |
+
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 6131 |
+
}
|
| 6132 |
+
self.compile_citation_identities()
|
| 6133 |
+
|
| 6134 |
+
authoritative = {}
|
| 6135 |
+
visible_any = {}
|
| 6136 |
+
for tag, metadata in (self.citation_map or {}).items():
|
| 6137 |
+
meta = metadata or {}
|
| 6138 |
+
marker = str(meta.get('in_text') or '').strip()
|
| 6139 |
+
if not marker:
|
| 6140 |
+
continue
|
| 6141 |
+
visible_any.setdefault(marker, []).append(str(tag))
|
| 6142 |
+
if str(meta.get('ref') or '').strip():
|
| 6143 |
+
authoritative.setdefault(marker, []).append(str(tag))
|
| 6144 |
+
|
| 6145 |
+
violations = []
|
| 6146 |
+
replacements = []
|
| 6147 |
+
for marker in list(dict.fromkeys(self._resolved_parenthetical_citations(value))):
|
| 6148 |
+
tags = list(authoritative.get(marker) or [])
|
| 6149 |
+
if not tags:
|
| 6150 |
+
visible_tags = sorted(visible_any.get(marker) or [], key=self._citation_tag_number)
|
| 6151 |
+
violations.append({
|
| 6152 |
+
'type': 'citation_resolved_marker_without_authority',
|
| 6153 |
+
'citation': marker,
|
| 6154 |
+
'tag': visible_tags[0] if len(visible_tags) == 1 else '',
|
| 6155 |
+
'sentence': marker,
|
| 6156 |
+
'reason': 'A reader-facing author-year citation was emitted without authoritative reference metadata.',
|
| 6157 |
+
'repair_instruction': (
|
| 6158 |
+
'Do not emit author-year citations directly. Use only an authorized CITE tag from this manuscript unit '
|
| 6159 |
+
'when its evidence directly supports the claim, or narrow/rewrite the claim so no unsupported citation is needed.'
|
| 6160 |
+
),
|
| 6161 |
+
})
|
| 6162 |
+
continue
|
| 6163 |
+
|
| 6164 |
+
distinct_sources = {
|
| 6165 |
+
self._citation_source_identity(self.citation_map.get(tag) or {})
|
| 6166 |
+
for tag in tags
|
| 6167 |
+
}
|
| 6168 |
+
if len(distinct_sources) != 1:
|
| 6169 |
+
violations.append({
|
| 6170 |
+
'type': 'citation_resolved_marker_ambiguous',
|
| 6171 |
+
'citation': marker,
|
| 6172 |
+
'sentence': marker,
|
| 6173 |
+
'reason': 'The reader-facing author-year marker maps to multiple distinct authoritative sources.',
|
| 6174 |
+
'repair_instruction': (
|
| 6175 |
+
'Do not guess which source was intended. Rewrite this claim using exactly one authorized CITE tag '
|
| 6176 |
+
'from the frozen evidence plan, or remove/narrow the unsupported citation-dependent claim.'
|
| 6177 |
+
),
|
| 6178 |
+
})
|
| 6179 |
+
continue
|
| 6180 |
+
|
| 6181 |
+
eligible = tags if allowed is None else [tag for tag in tags if tag in allowed]
|
| 6182 |
+
if not eligible:
|
| 6183 |
+
violations.append({
|
| 6184 |
+
'type': 'citation_resolved_marker_outside_frozen_section_whitelist',
|
| 6185 |
+
'citation': marker,
|
| 6186 |
+
'tag': sorted(tags, key=self._citation_tag_number)[0] if tags else '',
|
| 6187 |
+
'sentence': marker,
|
| 6188 |
+
'reason': 'The citation is authoritative globally but is not authorized for this manuscript unit.',
|
| 6189 |
+
'repair_instruction': (
|
| 6190 |
+
'Use only citation tags frozen for this manuscript unit. If no eligible source directly supports the claim, '
|
| 6191 |
+
'narrow or rewrite the claim without external citation support.'
|
| 6192 |
+
),
|
| 6193 |
+
})
|
| 6194 |
+
continue
|
| 6195 |
+
|
| 6196 |
+
selected = sorted(eligible, key=self._citation_tag_number)[0]
|
| 6197 |
+
replacements.append((marker, selected))
|
| 6198 |
+
self._append_citation_audit(
|
| 6199 |
+
section_name, 'normalize_exact_pre_resolved_marker_to_authoritative_tag',
|
| 6200 |
+
marker, selected, 'Exact compiled marker + authoritative reference + current-unit whitelist.'
|
| 6201 |
+
)
|
| 6202 |
+
|
| 6203 |
+
# Reject other high-confidence author-year forms rather than attempting a
|
| 6204 |
+
# lossy conversion to canonical identity. The bounded Section retry owns
|
| 6205 |
+
# prose repair and claim narrowing.
|
| 6206 |
+
for _start, _end, citation in self._high_confidence_noncanonical_citation_spans(value):
|
| 6207 |
+
violations.append({
|
| 6208 |
+
'type': 'citation_noncanonical_resolved_marker',
|
| 6209 |
+
'citation': citation,
|
| 6210 |
+
'sentence': citation,
|
| 6211 |
+
'reason': 'A direct author-year citation form bypasses the frozen CITE-tag authority path.',
|
| 6212 |
+
'repair_instruction': (
|
| 6213 |
+
'Do not emit narrative or bracketed author-year citations directly. Use one authorized CITE tag '
|
| 6214 |
+
'from this manuscript unit when directly supported, or rewrite the claim without that citation.'
|
| 6215 |
+
),
|
| 6216 |
+
})
|
| 6217 |
+
|
| 6218 |
+
for marker, tag in replacements:
|
| 6219 |
+
value = value.replace(marker, tag)
|
| 6220 |
+
return value, violations
|
| 6221 |
+
|
| 6222 |
+
def _quarantined_unit_citation_violations(self, text):
|
| 6223 |
+
"""Return typed citations found in a manuscript unit that forbids literature.
|
| 6224 |
+
|
| 6225 |
+
Conclusion/Appendix-style units are explicitly writer-quarantined from
|
| 6226 |
+
external literature. P3-A4 therefore treats any high-confidence raw or
|
| 6227 |
+
reader-facing citation form as a retry-owned scope violation instead of
|
| 6228 |
+
silently deleting the marker and leaving the surrounding claim intact.
|
| 6229 |
+
Bare years and ordinary numeric/model-version expressions are excluded by
|
| 6230 |
+
the same conservative scanners used by the normal citation path.
|
| 6231 |
+
"""
|
| 6232 |
+
value = str(text or '')
|
| 6233 |
+
found = []
|
| 6234 |
+
seen = set()
|
| 6235 |
+
|
| 6236 |
+
for raw in re.findall(r'\[?CITE-\d+\]?', value, flags=re.I):
|
| 6237 |
+
token = raw.upper()
|
| 6238 |
+
key = ('raw', token)
|
| 6239 |
+
if key not in seen:
|
| 6240 |
+
seen.add(key)
|
| 6241 |
+
found.append(token)
|
| 6242 |
+
|
| 6243 |
+
for marker in self._resolved_parenthetical_citations(value):
|
| 6244 |
+
key = ('resolved', marker)
|
| 6245 |
+
if key not in seen:
|
| 6246 |
+
seen.add(key)
|
| 6247 |
+
found.append(marker)
|
| 6248 |
+
|
| 6249 |
+
for _start, _end, citation in self._high_confidence_noncanonical_citation_spans(value):
|
| 6250 |
+
key = ('noncanonical', citation)
|
| 6251 |
+
if key not in seen:
|
| 6252 |
+
seen.add(key)
|
| 6253 |
+
found.append(citation)
|
| 6254 |
+
|
| 6255 |
+
return [{
|
| 6256 |
+
'type': 'citation_forbidden_in_quarantined_unit',
|
| 6257 |
+
'citation': citation,
|
| 6258 |
+
'sentence': citation,
|
| 6259 |
+
'reason': 'This manuscript unit forbids external literature citations, but a citation form was emitted.',
|
| 6260 |
+
'repair_instruction': (
|
| 6261 |
+
'Rewrite this unit without external literature citations. Preserve only documented study evidence, '
|
| 6262 |
+
'study-owned interpretation, and the section role; do not replace the citation with another source.'
|
| 6263 |
+
),
|
| 6264 |
+
} for citation in found]
|
| 6265 |
+
|
| 6266 |
def audit_and_resolve_citations(
|
| 6267 |
self, text, section_name='', section_id='', quarantine=False, allowed_tags=None
|
| 6268 |
):
|
|
|
|
| 6272 |
)
|
| 6273 |
value = self.strip_ineligible_current_study_citations(str(text or ''), section_name)
|
| 6274 |
if quarantine:
|
| 6275 |
+
quarantine_rows = self._quarantined_unit_citation_violations(value)
|
| 6276 |
+
if ownership_rows or quarantine_rows:
|
| 6277 |
+
# Do not silently delete a citation-dependent claim. The owning
|
| 6278 |
+
# Section retry/adaptive-repair path receives a typed violation
|
| 6279 |
+
# and must rewrite the unit without external literature.
|
| 6280 |
+
self._last_citation_semantic_violations = (
|
| 6281 |
+
list(ownership_rows) + list(quarantine_rows)
|
| 6282 |
+
)
|
| 6283 |
+
for row in quarantine_rows:
|
| 6284 |
+
self._append_citation_audit(
|
| 6285 |
+
section_name, 'reject_citation_in_quarantined_unit',
|
| 6286 |
+
str(row.get('citation') or ''), '', str(row.get('reason') or '')
|
| 6287 |
+
)
|
| 6288 |
+
return value
|
| 6289 |
+
return value
|
| 6290 |
+
|
| 6291 |
+
# P3-A4: collect deterministic citation-authority violations separately.
|
| 6292 |
+
# semantic_audit_claim_specific_citations() owns its own last-violation
|
| 6293 |
+
# buffer, so these rows are merged back only after that audit completes.
|
| 6294 |
+
authority_rows = []
|
| 6295 |
unknown = sorted({f"[{tag.upper()}]" for tag in re.findall(r'CITE-\d+', value, re.I)} - set(self.citation_map))
|
| 6296 |
if unknown:
|
| 6297 |
for tag in unknown:
|
| 6298 |
+
authority_rows.append({
|
| 6299 |
+
'type': 'citation_unknown_tag',
|
| 6300 |
+
'tag': tag,
|
| 6301 |
+
'sentence': tag,
|
| 6302 |
+
'reason': 'The manuscript unit emitted a citation capability that does not exist in authoritative citation_map.',
|
| 6303 |
+
'repair_instruction': (
|
| 6304 |
+
'Use only citation tags frozen for this manuscript unit, or narrow/rewrite the claim so no unsupported citation is needed.'
|
| 6305 |
+
),
|
| 6306 |
+
})
|
| 6307 |
value = re.sub(rf'\s*\[?{re.escape(tag.strip("[]"))}\]?\s*', ' ', value, flags=re.I)
|
| 6308 |
+
self._append_citation_audit(section_name, 'reject_unknown_tags_for_section_retry', text, value, ','.join(unknown))
|
| 6309 |
+
|
| 6310 |
allowed = None if allowed_tags is None else {
|
| 6311 |
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 6312 |
}
|
|
|
|
| 6326 |
self._append_citation_audit(
|
| 6327 |
section_name, 'reject_outside_frozen_section_whitelist', text, value, tag
|
| 6328 |
)
|
| 6329 |
+
|
| 6330 |
+
# P3-A4 closes the resolved-marker bypass before section sealing. Exact,
|
| 6331 |
+
# unambiguous, current-unit-authorized markers are normalized back to raw
|
| 6332 |
+
# capabilities so the existing claim-specific semantic audit still owns
|
| 6333 |
+
# claim/evidence adjudication. All other forms become typed violations.
|
| 6334 |
+
value, resolved_authority_rows = self._normalize_pre_resolved_citation_authority(
|
| 6335 |
+
value, allowed_tags=allowed, section_name=section_name
|
| 6336 |
+
)
|
| 6337 |
+
authority_rows.extend(resolved_authority_rows)
|
| 6338 |
+
|
| 6339 |
has_citations = bool(re.search(r'CITE-\d+', value, re.I))
|
| 6340 |
violations = self.find_multi_citation_violations(value)
|
| 6341 |
if has_citations and self.citation_semantic_audit.get('enabled', True):
|
|
|
|
| 6366 |
value = self._local_citation_recovery(value, section_name, retain_lexical=False)
|
| 6367 |
elif violations:
|
| 6368 |
value = self._local_citation_recovery(value, section_name)
|
| 6369 |
+
|
| 6370 |
+
pre_semantic_rows = list(ownership_rows) + list(outside_rows) + list(authority_rows)
|
| 6371 |
+
if pre_semantic_rows:
|
| 6372 |
self._last_citation_semantic_violations = (
|
| 6373 |
+
pre_semantic_rows + list(self._last_citation_semantic_violations or [])
|
|
|
|
|
|
|
| 6374 |
)
|
| 6375 |
+
|
| 6376 |
+
# P3-A4 reliability rule: once deterministic identity/scope authority has
|
| 6377 |
+
# already rejected this attempt, return it to the owning bounded Section
|
| 6378 |
+
# retry instead of letting the legacy multi-source hard exception preempt
|
| 6379 |
+
# typed recovery. The rejected bytes can never be sealed because runner
|
| 6380 |
+
# treats these rows as a hard pre-seal invariant.
|
| 6381 |
+
if outside_rows or authority_rows:
|
| 6382 |
+
return value
|
| 6383 |
+
|
| 6384 |
if self.find_multi_citation_violations(value):
|
| 6385 |
value = self._local_citation_recovery(value, section_name)
|
| 6386 |
if self.find_multi_citation_violations(value):
|