Spaces:
Sleeping
Sleeping
P3-A4 resilient citation repair
#153
by GuXiaopeng - opened
- core/generation_runner.py +118 -8
- core/pipeline_agent.py +362 -6
core/generation_runner.py
CHANGED
|
@@ -1902,6 +1902,21 @@ class GenerationRunner:
|
|
| 1902 |
rows.append(' | '.join(parts))
|
| 1903 |
return '\n'.join(f'- {row}' for row in rows)
|
| 1904 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1905 |
@staticmethod
|
| 1906 |
def _section_violation_severity(row, *, section_name: str = "", mode: str = "", exhausted: bool = False) -> str:
|
| 1907 |
"""Map one validation issue to REPAIRABLE/FATAL/ADVISORY.
|
|
@@ -1921,6 +1936,16 @@ class GenerationRunner:
|
|
| 1921 |
return "ADVISORY"
|
| 1922 |
return "REPAIRABLE"
|
| 1923 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1924 |
always_fatal = {
|
| 1925 |
"causal_language_violation",
|
| 1926 |
"sample_size_mismatch", "sample_count_mismatch", "scale_points_mismatch",
|
|
@@ -1929,7 +1954,12 @@ class GenerationRunner:
|
|
| 1929 |
"unsupported_study_procedure", "enumeration_contract_invalid",
|
| 1930 |
"hypothesis_contract_missing_id", "hypothesis_contract_ordinal_alias",
|
| 1931 |
"hypothesis_contract_pairing_mismatch", "hypothesis_contract_unknown_id",
|
| 1932 |
-
"citation_outside_frozen_section_whitelist",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1933 |
"study_design_scope_mismatch", "unsupported_focal_construct", "unsupported_relation_claim",
|
| 1934 |
"unsupported_moderation_claim", "unsupported_mediation_claim", "unsupported_temporal_scope",
|
| 1935 |
"unsupported_design_tradition", "unsupported_unit_of_analysis", "unsupported_data_source_claim",
|
|
@@ -1972,7 +2002,21 @@ class GenerationRunner:
|
|
| 1972 |
item = dict(row or {}) if isinstance(row, dict) else {"type": "unknown_validation_issue"}
|
| 1973 |
kind = str(item.get("type") or "unknown_validation_issue")
|
| 1974 |
groups = {
|
| 1975 |
-
"citation_scope": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1976 |
"citation_ownership": {"citation_evidence_ownership_mismatch"},
|
| 1977 |
"citation_support": {"citation_unsupported_claim", "citation_support_unverified"},
|
| 1978 |
"citation_compound": {"citation_claim_split_required"},
|
|
@@ -2110,6 +2154,20 @@ class GenerationRunner:
|
|
| 2110 |
blocking.append(row)
|
| 2111 |
return blocking, advisory
|
| 2112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2113 |
@staticmethod
|
| 2114 |
def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None:
|
| 2115 |
"""Append reader-facing, nonfatal notes without exposing control vocabulary."""
|
|
@@ -2177,6 +2235,7 @@ class GenerationRunner:
|
|
| 2177 |
self, *, agent, quant_engine, text: str, violations, mode: str, section_id: str,
|
| 2178 |
section_name: str, data_metadata: dict, study_evidence: str, evidence_contract: str,
|
| 2179 |
hypothesis_ledger: dict, task: dict, quote_invocation_plan: dict, quality_notes: list,
|
|
|
|
| 2180 |
):
|
| 2181 |
"""A2 last-mile semantic repair after lineage retry exhaustion.
|
| 2182 |
|
|
@@ -2215,10 +2274,22 @@ class GenerationRunner:
|
|
| 2215 |
repaired, _enum_rows = agent.repair_and_validate_enumeration_contract(
|
| 2216 |
repaired, section_name=section_name
|
| 2217 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2218 |
rows = self._section_validation_bundle(
|
| 2219 |
mode, agent, quant_engine, repaired, data_metadata, section_id=section_id,
|
| 2220 |
section_name=section_name, evidence_contract=evidence_contract, include_critic=False,
|
| 2221 |
-
hypothesis_ledger=hypothesis_ledger,
|
| 2222 |
)
|
| 2223 |
blocking, advisory = self._partition_section_violations(
|
| 2224 |
rows, section_name=section_name, mode=mode, exhausted=True
|
|
@@ -5600,11 +5671,8 @@ class GenerationRunner:
|
|
| 5600 |
)
|
| 5601 |
sec_title = sec["section"]
|
| 5602 |
subsection = sec.get("subsection")
|
| 5603 |
-
is_quarantined = any(
|
| 5604 |
-
word in sec_title.lower()
|
| 5605 |
-
for word in ["abstract", "keyword", "conclusion", "references", "appendix"]
|
| 5606 |
-
)
|
| 5607 |
section_id = str(sec.get("section_id") or "")
|
|
|
|
| 5608 |
section_logical_node = self._section_logical_node(
|
| 5609 |
mode, section_id, sec.get("subsection_id")
|
| 5610 |
)
|
|
@@ -5630,7 +5698,7 @@ class GenerationRunner:
|
|
| 5630 |
elif section_id == "conclusion":
|
| 5631 |
mode_directive = str(APP_CONFIG.get("conclusion_anchor_directive") or "")
|
| 5632 |
|
| 5633 |
-
if is_quarantined
|
| 5634 |
citation_rule = (
|
| 5635 |
"NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. "
|
| 5636 |
"Report only documented empirical/qualitative evidence and the interpretation permitted by the section role."
|
|
@@ -6097,6 +6165,29 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6097 |
if adaptive_advisory:
|
| 6098 |
self._record_section_advisories(quality_notes, adaptive_advisory, section_name=display_name)
|
| 6099 |
if adaptive_blocking:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6100 |
self._record_adaptive_completion_residual(
|
| 6101 |
quality_notes, adaptive_blocking, section_name=display_name, stage="phase1_semantic_repair"
|
| 6102 |
)
|
|
@@ -6276,7 +6367,26 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6276 |
study_evidence=writer_scoped_data_md, evidence_contract=evidence_packet,
|
| 6277 |
hypothesis_ledger=hypothesis_ledger, task=sec,
|
| 6278 |
quote_invocation_plan=quote_invocation_plan, quality_notes=quality_notes,
|
|
|
|
|
|
|
| 6279 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6280 |
logging.warning(
|
| 6281 |
"Adaptive completion accepted section | unit=%s residual=%s",
|
| 6282 |
display_name, len(residual_rows),
|
|
|
|
| 1902 |
rows.append(' | '.join(parts))
|
| 1903 |
return '\n'.join(f'- {row}' for row in rows)
|
| 1904 |
|
| 1905 |
+
@staticmethod
|
| 1906 |
+
def _section_forbids_external_citations(section_title: str = "", section_id: str = "") -> bool:
|
| 1907 |
+
"""One authority for manuscript units that must remain citation-free.
|
| 1908 |
+
|
| 1909 |
+
P3-A4 closes a historic split where Results/Findings were declared
|
| 1910 |
+
citation-free in the Writer prompt but did not enter the same quarantine
|
| 1911 |
+
validation path as Conclusion/Appendix-style units.
|
| 1912 |
+
"""
|
| 1913 |
+
title = str(section_title or '').casefold()
|
| 1914 |
+
sid = str(section_id or '').strip().casefold()
|
| 1915 |
+
return (
|
| 1916 |
+
any(word in title for word in ("abstract", "keyword", "conclusion", "references", "appendix"))
|
| 1917 |
+
or sid in {"results", "findings"}
|
| 1918 |
+
)
|
| 1919 |
+
|
| 1920 |
@staticmethod
|
| 1921 |
def _section_violation_severity(row, *, section_name: str = "", mode: str = "", exhausted: bool = False) -> str:
|
| 1922 |
"""Map one validation issue to REPAIRABLE/FATAL/ADVISORY.
|
|
|
|
| 1936 |
return "ADVISORY"
|
| 1937 |
return "REPAIRABLE"
|
| 1938 |
|
| 1939 |
+
# Delivery-first resilience: uncertain noncanonical citation-like forms
|
| 1940 |
+
# get a DeepSeek repair opportunity, but they are not allowed to terminate
|
| 1941 |
+
# an otherwise complete manuscript after bounded repair. Definite
|
| 1942 |
+
# authority/scope violations remain fail-closed below.
|
| 1943 |
+
if kind in {
|
| 1944 |
+
"citation_noncanonical_unverified_marker",
|
| 1945 |
+
"citation_suspected_in_quarantined_unit",
|
| 1946 |
+
}:
|
| 1947 |
+
return "ADVISORY"
|
| 1948 |
+
|
| 1949 |
always_fatal = {
|
| 1950 |
"causal_language_violation",
|
| 1951 |
"sample_size_mismatch", "sample_count_mismatch", "scale_points_mismatch",
|
|
|
|
| 1954 |
"unsupported_study_procedure", "enumeration_contract_invalid",
|
| 1955 |
"hypothesis_contract_missing_id", "hypothesis_contract_ordinal_alias",
|
| 1956 |
"hypothesis_contract_pairing_mismatch", "hypothesis_contract_unknown_id",
|
| 1957 |
+
"citation_outside_frozen_section_whitelist",
|
| 1958 |
+
"citation_resolved_marker_outside_frozen_section_whitelist",
|
| 1959 |
+
"citation_unknown_tag", "citation_resolved_marker_without_authority",
|
| 1960 |
+
"citation_resolved_marker_ambiguous", "citation_noncanonical_resolved_marker",
|
| 1961 |
+
"citation_forbidden_in_quarantined_unit",
|
| 1962 |
+
"authorial_ai_meta_trace",
|
| 1963 |
"study_design_scope_mismatch", "unsupported_focal_construct", "unsupported_relation_claim",
|
| 1964 |
"unsupported_moderation_claim", "unsupported_mediation_claim", "unsupported_temporal_scope",
|
| 1965 |
"unsupported_design_tradition", "unsupported_unit_of_analysis", "unsupported_data_source_claim",
|
|
|
|
| 2002 |
item = dict(row or {}) if isinstance(row, dict) else {"type": "unknown_validation_issue"}
|
| 2003 |
kind = str(item.get("type") or "unknown_validation_issue")
|
| 2004 |
groups = {
|
| 2005 |
+
"citation_scope": {
|
| 2006 |
+
"citation_outside_frozen_section_whitelist",
|
| 2007 |
+
"citation_resolved_marker_outside_frozen_section_whitelist",
|
| 2008 |
+
},
|
| 2009 |
+
"citation_authority": {
|
| 2010 |
+
"citation_unknown_tag",
|
| 2011 |
+
"citation_resolved_marker_without_authority",
|
| 2012 |
+
"citation_resolved_marker_ambiguous",
|
| 2013 |
+
"citation_noncanonical_resolved_marker",
|
| 2014 |
+
"citation_forbidden_in_quarantined_unit",
|
| 2015 |
+
},
|
| 2016 |
+
"citation_style": {
|
| 2017 |
+
"citation_noncanonical_unverified_marker",
|
| 2018 |
+
"citation_suspected_in_quarantined_unit",
|
| 2019 |
+
},
|
| 2020 |
"citation_ownership": {"citation_evidence_ownership_mismatch"},
|
| 2021 |
"citation_support": {"citation_unsupported_claim", "citation_support_unverified"},
|
| 2022 |
"citation_compound": {"citation_claim_split_required"},
|
|
|
|
| 2154 |
blocking.append(row)
|
| 2155 |
return blocking, advisory
|
| 2156 |
|
| 2157 |
+
@classmethod
|
| 2158 |
+
def _persistent_citation_authority_rows(cls, rows) -> list[dict]:
|
| 2159 |
+
"""Return citation identity/scope defects that may never cross a Section seal.
|
| 2160 |
+
|
| 2161 |
+
P3-A4 intentionally keeps this narrower than general validation failure.
|
| 2162 |
+
Citation support/style advisories retain their existing adaptive-completion
|
| 2163 |
+
behavior; only source identity and unit-scope authority stay fail-closed.
|
| 2164 |
+
"""
|
| 2165 |
+
return [
|
| 2166 |
+
row for row in list(rows or [])
|
| 2167 |
+
if isinstance(row, dict)
|
| 2168 |
+
and cls._section_violation_lineage(row) in {"citation_scope", "citation_authority"}
|
| 2169 |
+
]
|
| 2170 |
+
|
| 2171 |
@staticmethod
|
| 2172 |
def _record_section_advisories(quality_notes: list, rows, *, section_name: str = "") -> None:
|
| 2173 |
"""Append reader-facing, nonfatal notes without exposing control vocabulary."""
|
|
|
|
| 2235 |
self, *, agent, quant_engine, text: str, violations, mode: str, section_id: str,
|
| 2236 |
section_name: str, data_metadata: dict, study_evidence: str, evidence_contract: str,
|
| 2237 |
hypothesis_ledger: dict, task: dict, quote_invocation_plan: dict, quality_notes: list,
|
| 2238 |
+
allowed_citation_tags=None, quarantine=False,
|
| 2239 |
):
|
| 2240 |
"""A2 last-mile semantic repair after lineage retry exhaustion.
|
| 2241 |
|
|
|
|
| 2274 |
repaired, _enum_rows = agent.repair_and_validate_enumeration_contract(
|
| 2275 |
repaired, section_name=section_name
|
| 2276 |
)
|
| 2277 |
+
# P3-A4: every model-owned last-mile rewrite must re-enter the same
|
| 2278 |
+
# citation authority + claim-specific semantic Gate before it can be
|
| 2279 |
+
# considered sealable. Otherwise an unrelated adaptive repair could
|
| 2280 |
+
# reintroduce a pre-resolved/unknown/wrong-scope citation after the
|
| 2281 |
+
# earlier Section citation audit had already passed.
|
| 2282 |
+
repaired = agent.audit_and_resolve_citations(
|
| 2283 |
+
repaired, section_name=section_name, section_id=section_id,
|
| 2284 |
+
quarantine=quarantine, allowed_tags=allowed_citation_tags,
|
| 2285 |
+
)
|
| 2286 |
+
adaptive_citation_rows = copy.deepcopy(
|
| 2287 |
+
list(getattr(agent, "_last_citation_semantic_violations", []) or [])
|
| 2288 |
+
)
|
| 2289 |
rows = self._section_validation_bundle(
|
| 2290 |
mode, agent, quant_engine, repaired, data_metadata, section_id=section_id,
|
| 2291 |
section_name=section_name, evidence_contract=evidence_contract, include_critic=False,
|
| 2292 |
+
citation_violations=adaptive_citation_rows, hypothesis_ledger=hypothesis_ledger,
|
| 2293 |
)
|
| 2294 |
blocking, advisory = self._partition_section_violations(
|
| 2295 |
rows, section_name=section_name, mode=mode, exhausted=True
|
|
|
|
| 5671 |
)
|
| 5672 |
sec_title = sec["section"]
|
| 5673 |
subsection = sec.get("subsection")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5674 |
section_id = str(sec.get("section_id") or "")
|
| 5675 |
+
is_quarantined = self._section_forbids_external_citations(sec_title, section_id)
|
| 5676 |
section_logical_node = self._section_logical_node(
|
| 5677 |
mode, section_id, sec.get("subsection_id")
|
| 5678 |
)
|
|
|
|
| 5698 |
elif section_id == "conclusion":
|
| 5699 |
mode_directive = str(APP_CONFIG.get("conclusion_anchor_directive") or "")
|
| 5700 |
|
| 5701 |
+
if is_quarantined:
|
| 5702 |
citation_rule = (
|
| 5703 |
"NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. "
|
| 5704 |
"Report only documented empirical/qualitative evidence and the interpretation permitted by the section role."
|
|
|
|
| 6165 |
if adaptive_advisory:
|
| 6166 |
self._record_section_advisories(quality_notes, adaptive_advisory, section_name=display_name)
|
| 6167 |
if adaptive_blocking:
|
| 6168 |
+
# P3-A4: citation identity/scope authority is a hard
|
| 6169 |
+
# pre-seal invariant. Adaptive completion may retain
|
| 6170 |
+
# reader-facing advisories for unrelated quality
|
| 6171 |
+
# defects, but it may never seal a Section that still
|
| 6172 |
+
# contains an unauthorized/ambiguous citation identity
|
| 6173 |
+
# or a citation outside the frozen unit whitelist.
|
| 6174 |
+
persistent_citation_authority = self._persistent_citation_authority_rows(
|
| 6175 |
+
adaptive_blocking
|
| 6176 |
+
)
|
| 6177 |
+
if persistent_citation_authority:
|
| 6178 |
+
self._checkpoint_section_retry_exhausted(
|
| 6179 |
+
state, section_id=section_id, section_name=display_name,
|
| 6180 |
+
violations=persistent_citation_authority, pct=pct, attempts=section_attempt,
|
| 6181 |
+
)
|
| 6182 |
+
raise RuntimeError(
|
| 6183 |
+
"citation_authority_repair_exhausted:"
|
| 6184 |
+
+ str(display_name)
|
| 6185 |
+
+ ":"
|
| 6186 |
+
+ ",".join(sorted({
|
| 6187 |
+
str((row or {}).get("type") or "citation_authority")
|
| 6188 |
+
for row in persistent_citation_authority if isinstance(row, dict)
|
| 6189 |
+
}))
|
| 6190 |
+
)
|
| 6191 |
self._record_adaptive_completion_residual(
|
| 6192 |
quality_notes, adaptive_blocking, section_name=display_name, stage="phase1_semantic_repair"
|
| 6193 |
)
|
|
|
|
| 6367 |
study_evidence=writer_scoped_data_md, evidence_contract=evidence_packet,
|
| 6368 |
hypothesis_ledger=hypothesis_ledger, task=sec,
|
| 6369 |
quote_invocation_plan=quote_invocation_plan, quality_notes=quality_notes,
|
| 6370 |
+
allowed_citation_tags=allowed_section_citation_tags,
|
| 6371 |
+
quarantine=is_quarantined,
|
| 6372 |
)
|
| 6373 |
+
persistent_citation_authority = self._persistent_citation_authority_rows(
|
| 6374 |
+
residual_rows
|
| 6375 |
+
)
|
| 6376 |
+
if persistent_citation_authority:
|
| 6377 |
+
self._checkpoint_section_retry_exhausted(
|
| 6378 |
+
state, section_id=section_id, section_name=display_name,
|
| 6379 |
+
violations=persistent_citation_authority, pct=pct, attempts=section_attempt,
|
| 6380 |
+
)
|
| 6381 |
+
raise RuntimeError(
|
| 6382 |
+
"citation_authority_repair_exhausted:"
|
| 6383 |
+
+ str(display_name)
|
| 6384 |
+
+ ":"
|
| 6385 |
+
+ ",".join(sorted({
|
| 6386 |
+
str((row or {}).get("type") or "citation_authority")
|
| 6387 |
+
for row in persistent_citation_authority if isinstance(row, dict)
|
| 6388 |
+
}))
|
| 6389 |
+
)
|
| 6390 |
logging.warning(
|
| 6391 |
"Adaptive completion accepted section | unit=%s residual=%s",
|
| 6392 |
display_name, len(residual_rows),
|
core/pipeline_agent.py
CHANGED
|
@@ -6349,6 +6349,314 @@ 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 +6666,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 +6720,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 +6760,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):
|
|
|
|
| 6349 |
)
|
| 6350 |
return revised
|
| 6351 |
|
| 6352 |
+
@staticmethod
|
| 6353 |
+
def _high_confidence_noncanonical_citation_spans(text):
|
| 6354 |
+
"""Return only *suspected* noncanonical citation forms worth one soft repair pass.
|
| 6355 |
+
|
| 6356 |
+
P3-A4-R4 delivery-first rule:
|
| 6357 |
+
- exact aliases of authoritative citation_map entries are handled separately
|
| 6358 |
+
and deterministically by ``_normalize_pre_resolved_citation_authority``;
|
| 6359 |
+
- unmatched narrative ``Name (year)`` forms are surfaced only when nearby
|
| 6360 |
+
attribution language makes citation intent high-confidence;
|
| 6361 |
+
- bracketed ``[Name, year]`` forms remain suspicious, but are treated as
|
| 6362 |
+
repairable/advisory rather than a terminal authority failure when they do
|
| 6363 |
+
not map to citation_map.
|
| 6364 |
+
|
| 6365 |
+
This deliberately avoids treating ordinary product/method/version prose such
|
| 6366 |
+
as ``Stata (2024)``, ``Mplus (2024)``, ``SmartPLS (2024)``,
|
| 6367 |
+
``DeepSeek (2024)``, or ``SEM (2024)`` as citations merely because the
|
| 6368 |
+
surface form resembles an author-year citation.
|
| 6369 |
+
"""
|
| 6370 |
+
value = str(text or '')
|
| 6371 |
+
surname = r"[A-ZÀ-ÖØ-Þ][A-Za-zÀ-ÖØ-öø-ÿ'’.-]{1,39}"
|
| 6372 |
+
conjunction = r"(?:&|and|AND)"
|
| 6373 |
+
et_al = r"(?:et|Et|ET)\s+(?:al|Al|AL)\."
|
| 6374 |
+
author_group = rf"(?:{surname}(?:\s+{conjunction}\s+{surname})?|{surname}\s+{et_al})"
|
| 6375 |
+
year = r"(?:19|20)\d{2}[a-z]?"
|
| 6376 |
+
bracketed = re.compile(rf"\[(?P<cite>{author_group}\s*,\s*{year})\]")
|
| 6377 |
+
narrative = re.compile(rf"(?<![\w\[])(?P<cite>{author_group})\s*\(\s*(?P<year>{year})\s*\)")
|
| 6378 |
+
attribution_after = re.compile(
|
| 6379 |
+
r"^\s*(?:,\s*)?(?:argues?|reports?|finds?|found|suggests?|shows?|showed|"
|
| 6380 |
+
r"demonstrates?|demonstrated|proposes?|notes?|observes?|states?|concludes?|"
|
| 6381 |
+
r"documents?|identifies?|reported|suggested|proposed|noted|observed|stated|"
|
| 6382 |
+
r"concluded|documented|identified)\b",
|
| 6383 |
+
re.I,
|
| 6384 |
+
)
|
| 6385 |
+
attribution_before = re.compile(
|
| 6386 |
+
r"(?:according\s+to|as\s+(?:reported|noted|shown|argued|observed|stated)\s+by)\s*$",
|
| 6387 |
+
re.I,
|
| 6388 |
+
)
|
| 6389 |
+
|
| 6390 |
+
spans = []
|
| 6391 |
+
for match in bracketed.finditer(value):
|
| 6392 |
+
spans.append((match.start(), match.end(), match.group(0).strip()))
|
| 6393 |
+
for match in narrative.finditer(value):
|
| 6394 |
+
before = value[max(0, match.start() - 48):match.start()]
|
| 6395 |
+
after = value[match.end():min(len(value), match.end() + 64)]
|
| 6396 |
+
if attribution_after.search(after) or attribution_before.search(before):
|
| 6397 |
+
spans.append((match.start(), match.end(), match.group(0).strip()))
|
| 6398 |
+
|
| 6399 |
+
unique = []
|
| 6400 |
+
seen = set()
|
| 6401 |
+
for row in sorted(spans, key=lambda item: (item[0], item[1], item[2])):
|
| 6402 |
+
key = (row[0], row[1], row[2])
|
| 6403 |
+
if key not in seen:
|
| 6404 |
+
seen.add(key)
|
| 6405 |
+
unique.append(row)
|
| 6406 |
+
return unique
|
| 6407 |
+
|
| 6408 |
+
def _authoritative_noncanonical_citation_aliases(self):
|
| 6409 |
+
"""Map exact narrative/bracketed aliases to authoritative CITE tags.
|
| 6410 |
+
|
| 6411 |
+
The alias set is derived only from compiled citation_map markers carrying
|
| 6412 |
+
authoritative reference metadata. No free-form author/year guessing is
|
| 6413 |
+
performed.
|
| 6414 |
+
"""
|
| 6415 |
+
self.compile_citation_identities()
|
| 6416 |
+
aliases = {}
|
| 6417 |
+
for tag, metadata in (self.citation_map or {}).items():
|
| 6418 |
+
meta = metadata or {}
|
| 6419 |
+
marker = str(meta.get('in_text') or '').strip()
|
| 6420 |
+
if not marker or not str(meta.get('ref') or '').strip():
|
| 6421 |
+
continue
|
| 6422 |
+
match = re.fullmatch(
|
| 6423 |
+
r"\((?P<authors>[^()]+?)\s*,\s*(?P<year>(?:19|20)\d{2}[a-z]?)\)",
|
| 6424 |
+
marker,
|
| 6425 |
+
flags=re.I,
|
| 6426 |
+
)
|
| 6427 |
+
if not match:
|
| 6428 |
+
continue
|
| 6429 |
+
authors = match.group('authors').strip()
|
| 6430 |
+
year = match.group('year').strip()
|
| 6431 |
+
for alias in (f"{authors} ({year})", f"[{authors}, {year}]"):
|
| 6432 |
+
aliases.setdefault(alias, []).append(str(tag))
|
| 6433 |
+
return aliases
|
| 6434 |
+
def _normalize_pre_resolved_citation_authority(self, text, *, allowed_tags=None, section_name=''):
|
| 6435 |
+
"""Normalize only exact, authoritative pre-resolved markers back to CITE tags.
|
| 6436 |
+
|
| 6437 |
+
The Section Writer is supposed to emit frozen ``[CITE-XX]`` capabilities.
|
| 6438 |
+
A model-emitted reader-facing author-year marker therefore has no provenance
|
| 6439 |
+
until it is matched exactly against the *compiled* citation authority.
|
| 6440 |
+
|
| 6441 |
+
Safe normalization requires all of the following:
|
| 6442 |
+
- exact marker equality after ``compile_citation_identities()``;
|
| 6443 |
+
- authoritative reference metadata;
|
| 6444 |
+
- one distinct source identity for the visible marker;
|
| 6445 |
+
- at least one matching tag inside this manuscript unit's whitelist.
|
| 6446 |
+
|
| 6447 |
+
Anything else becomes a typed violation. No fuzzy author/year matching,
|
| 6448 |
+
metadata synthesis, or silent citation deletion is permitted here.
|
| 6449 |
+
"""
|
| 6450 |
+
value = str(text or '')
|
| 6451 |
+
allowed = None if allowed_tags is None else {
|
| 6452 |
+
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 6453 |
+
}
|
| 6454 |
+
self.compile_citation_identities()
|
| 6455 |
+
|
| 6456 |
+
authoritative = {}
|
| 6457 |
+
visible_any = {}
|
| 6458 |
+
for tag, metadata in (self.citation_map or {}).items():
|
| 6459 |
+
meta = metadata or {}
|
| 6460 |
+
marker = str(meta.get('in_text') or '').strip()
|
| 6461 |
+
if not marker:
|
| 6462 |
+
continue
|
| 6463 |
+
visible_any.setdefault(marker, []).append(str(tag))
|
| 6464 |
+
if str(meta.get('ref') or '').strip():
|
| 6465 |
+
authoritative.setdefault(marker, []).append(str(tag))
|
| 6466 |
+
|
| 6467 |
+
violations = []
|
| 6468 |
+
replacements = []
|
| 6469 |
+
for marker in list(dict.fromkeys(self._resolved_parenthetical_citations(value))):
|
| 6470 |
+
tags = list(authoritative.get(marker) or [])
|
| 6471 |
+
if not tags:
|
| 6472 |
+
visible_tags = sorted(visible_any.get(marker) or [], key=self._citation_tag_number)
|
| 6473 |
+
violations.append({
|
| 6474 |
+
'type': 'citation_resolved_marker_without_authority',
|
| 6475 |
+
'citation': marker,
|
| 6476 |
+
'tag': visible_tags[0] if len(visible_tags) == 1 else '',
|
| 6477 |
+
'sentence': marker,
|
| 6478 |
+
'reason': 'A reader-facing author-year citation was emitted without authoritative reference metadata.',
|
| 6479 |
+
'repair_instruction': (
|
| 6480 |
+
'Do not emit author-year citations directly. Use only an authorized CITE tag from this manuscript unit '
|
| 6481 |
+
'when its evidence directly supports the claim, or narrow/rewrite the claim so no unsupported citation is needed.'
|
| 6482 |
+
),
|
| 6483 |
+
})
|
| 6484 |
+
continue
|
| 6485 |
+
|
| 6486 |
+
distinct_sources = {
|
| 6487 |
+
self._citation_source_identity(self.citation_map.get(tag) or {})
|
| 6488 |
+
for tag in tags
|
| 6489 |
+
}
|
| 6490 |
+
if len(distinct_sources) != 1:
|
| 6491 |
+
violations.append({
|
| 6492 |
+
'type': 'citation_resolved_marker_ambiguous',
|
| 6493 |
+
'citation': marker,
|
| 6494 |
+
'sentence': marker,
|
| 6495 |
+
'reason': 'The reader-facing author-year marker maps to multiple distinct authoritative sources.',
|
| 6496 |
+
'repair_instruction': (
|
| 6497 |
+
'Do not guess which source was intended. Rewrite this claim using exactly one authorized CITE tag '
|
| 6498 |
+
'from the frozen evidence plan, or remove/narrow the unsupported citation-dependent claim.'
|
| 6499 |
+
),
|
| 6500 |
+
})
|
| 6501 |
+
continue
|
| 6502 |
+
|
| 6503 |
+
eligible = tags if allowed is None else [tag for tag in tags if tag in allowed]
|
| 6504 |
+
if not eligible:
|
| 6505 |
+
violations.append({
|
| 6506 |
+
'type': 'citation_resolved_marker_outside_frozen_section_whitelist',
|
| 6507 |
+
'citation': marker,
|
| 6508 |
+
'tag': sorted(tags, key=self._citation_tag_number)[0] if tags else '',
|
| 6509 |
+
'sentence': marker,
|
| 6510 |
+
'reason': 'The citation is authoritative globally but is not authorized for this manuscript unit.',
|
| 6511 |
+
'repair_instruction': (
|
| 6512 |
+
'Use only citation tags frozen for this manuscript unit. If no eligible source directly supports the claim, '
|
| 6513 |
+
'narrow or rewrite the claim without external citation support.'
|
| 6514 |
+
),
|
| 6515 |
+
})
|
| 6516 |
+
continue
|
| 6517 |
+
|
| 6518 |
+
selected = sorted(eligible, key=self._citation_tag_number)[0]
|
| 6519 |
+
replacements.append((marker, selected))
|
| 6520 |
+
self._append_citation_audit(
|
| 6521 |
+
section_name, 'normalize_exact_pre_resolved_marker_to_authoritative_tag',
|
| 6522 |
+
marker, selected, 'Exact compiled marker + authoritative reference + current-unit whitelist.'
|
| 6523 |
+
)
|
| 6524 |
+
|
| 6525 |
+
# Exact narrative/bracketed aliases of authoritative citation_map entries
|
| 6526 |
+
# are a formatting defect, not a reason to kill generation. Normalize
|
| 6527 |
+
# them deterministically back to the owning CITE tag and let the existing
|
| 6528 |
+
# claim-specific semantic audit validate the claim/source relationship.
|
| 6529 |
+
alias_authority = self._authoritative_noncanonical_citation_aliases()
|
| 6530 |
+
authoritative_aliases_seen = set()
|
| 6531 |
+
for alias, alias_tags in alias_authority.items():
|
| 6532 |
+
if alias not in value:
|
| 6533 |
+
continue
|
| 6534 |
+
tags = list(alias_tags or [])
|
| 6535 |
+
authoritative_aliases_seen.add(alias)
|
| 6536 |
+
distinct_sources = {
|
| 6537 |
+
self._citation_source_identity(self.citation_map.get(tag) or {})
|
| 6538 |
+
for tag in tags
|
| 6539 |
+
}
|
| 6540 |
+
if len(distinct_sources) != 1:
|
| 6541 |
+
violations.append({
|
| 6542 |
+
'type': 'citation_resolved_marker_ambiguous',
|
| 6543 |
+
'citation': alias,
|
| 6544 |
+
'sentence': alias,
|
| 6545 |
+
'reason': 'The noncanonical author-year marker maps to multiple distinct authoritative sources.',
|
| 6546 |
+
'repair_instruction': (
|
| 6547 |
+
'Do not guess which source was intended. Rewrite this claim using exactly one authorized CITE tag '
|
| 6548 |
+
'from the frozen evidence plan, or remove/narrow the unsupported citation-dependent claim.'
|
| 6549 |
+
),
|
| 6550 |
+
})
|
| 6551 |
+
continue
|
| 6552 |
+
|
| 6553 |
+
eligible = tags if allowed is None else [tag for tag in tags if tag in allowed]
|
| 6554 |
+
if not eligible:
|
| 6555 |
+
violations.append({
|
| 6556 |
+
'type': 'citation_resolved_marker_outside_frozen_section_whitelist',
|
| 6557 |
+
'citation': alias,
|
| 6558 |
+
'tag': sorted(tags, key=self._citation_tag_number)[0] if tags else '',
|
| 6559 |
+
'sentence': alias,
|
| 6560 |
+
'reason': 'The citation is authoritative globally but is not authorized for this manuscript unit.',
|
| 6561 |
+
'repair_instruction': (
|
| 6562 |
+
'Use only citation tags frozen for this manuscript unit. If no eligible source directly supports the claim, '
|
| 6563 |
+
'narrow or rewrite the claim without external citation support.'
|
| 6564 |
+
),
|
| 6565 |
+
})
|
| 6566 |
+
continue
|
| 6567 |
+
|
| 6568 |
+
selected = sorted(eligible, key=self._citation_tag_number)[0]
|
| 6569 |
+
replacements.append((alias, selected))
|
| 6570 |
+
self._append_citation_audit(
|
| 6571 |
+
section_name, 'normalize_exact_noncanonical_marker_to_authoritative_tag',
|
| 6572 |
+
alias, selected, 'Exact authoritative alias + current-unit whitelist.'
|
| 6573 |
+
)
|
| 6574 |
+
|
| 6575 |
+
# Unmatched suspicious author-year forms get one DeepSeek-owned repair
|
| 6576 |
+
# opportunity but are explicitly *soft* after bounded repair. This keeps
|
| 6577 |
+
# the system resilient when a product/method/model label happens to look
|
| 6578 |
+
# like a surname + year while still asking the model to clean genuinely
|
| 6579 |
+
# citation-like prose when attribution context is strong.
|
| 6580 |
+
for _start, _end, citation in self._high_confidence_noncanonical_citation_spans(value):
|
| 6581 |
+
if citation in authoritative_aliases_seen:
|
| 6582 |
+
continue
|
| 6583 |
+
violations.append({
|
| 6584 |
+
'type': 'citation_noncanonical_unverified_marker',
|
| 6585 |
+
'citation': citation,
|
| 6586 |
+
'sentence': citation,
|
| 6587 |
+
'reason': 'A citation-like author-year form could not be bound exactly to frozen citation authority.',
|
| 6588 |
+
'repair_instruction': (
|
| 6589 |
+
'If this is intended as a literature citation, replace it with one authorized CITE tag whose evidence directly '
|
| 6590 |
+
'supports the claim. If it is a product, method, model, software, or ordinary year label, keep the factual prose '
|
| 6591 |
+
'but rewrite it so it cannot be mistaken for an external author-year citation.'
|
| 6592 |
+
),
|
| 6593 |
+
})
|
| 6594 |
+
|
| 6595 |
+
for marker, tag in replacements:
|
| 6596 |
+
value = value.replace(marker, tag)
|
| 6597 |
+
return value, violations
|
| 6598 |
+
def _quarantined_unit_citation_violations(self, text):
|
| 6599 |
+
"""Return definite and suspected citation forms in citation-free units.
|
| 6600 |
+
|
| 6601 |
+
Delivery-first classification:
|
| 6602 |
+
- raw CITE tags, canonical resolved parenthetical markers, and exact
|
| 6603 |
+
authoritative narrative/bracketed aliases are definite literature
|
| 6604 |
+
citations and remain fail-closed after bounded repair;
|
| 6605 |
+
- unmatched citation-like narrative/bracketed forms are only suspected.
|
| 6606 |
+
They receive a DeepSeek repair opportunity but may degrade to an
|
| 6607 |
+
advisory instead of terminating an otherwise complete manuscript.
|
| 6608 |
+
"""
|
| 6609 |
+
value = str(text or '')
|
| 6610 |
+
rows = []
|
| 6611 |
+
seen = set()
|
| 6612 |
+
|
| 6613 |
+
def add(kind, citation, reason, instruction):
|
| 6614 |
+
key = (kind, str(citation or ''))
|
| 6615 |
+
if key in seen:
|
| 6616 |
+
return
|
| 6617 |
+
seen.add(key)
|
| 6618 |
+
rows.append({
|
| 6619 |
+
'type': kind,
|
| 6620 |
+
'citation': str(citation or ''),
|
| 6621 |
+
'sentence': str(citation or ''),
|
| 6622 |
+
'reason': reason,
|
| 6623 |
+
'repair_instruction': instruction,
|
| 6624 |
+
})
|
| 6625 |
+
|
| 6626 |
+
definite_reason = 'This manuscript unit forbids external literature citations, but a definite citation form was emitted.'
|
| 6627 |
+
definite_instruction = (
|
| 6628 |
+
'Rewrite this unit without external literature citations. Preserve only documented study evidence, '
|
| 6629 |
+
'study-owned interpretation, and the section role; do not replace the citation with another source.'
|
| 6630 |
+
)
|
| 6631 |
+
|
| 6632 |
+
for raw in re.findall(r'\[?CITE-\d+\]?', value, flags=re.I):
|
| 6633 |
+
add('citation_forbidden_in_quarantined_unit', raw.upper(), definite_reason, definite_instruction)
|
| 6634 |
+
|
| 6635 |
+
for marker in self._resolved_parenthetical_citations(value):
|
| 6636 |
+
add('citation_forbidden_in_quarantined_unit', marker, definite_reason, definite_instruction)
|
| 6637 |
+
|
| 6638 |
+
authoritative_aliases = self._authoritative_noncanonical_citation_aliases()
|
| 6639 |
+
authoritative_seen = set()
|
| 6640 |
+
for alias in authoritative_aliases:
|
| 6641 |
+
if alias in value:
|
| 6642 |
+
authoritative_seen.add(alias)
|
| 6643 |
+
add('citation_forbidden_in_quarantined_unit', alias, definite_reason, definite_instruction)
|
| 6644 |
+
|
| 6645 |
+
for _start, _end, citation in self._high_confidence_noncanonical_citation_spans(value):
|
| 6646 |
+
if citation in authoritative_seen:
|
| 6647 |
+
continue
|
| 6648 |
+
add(
|
| 6649 |
+
'citation_suspected_in_quarantined_unit',
|
| 6650 |
+
citation,
|
| 6651 |
+
'A citation-like author-year form appeared in a citation-free manuscript unit but was not bound to frozen authority.',
|
| 6652 |
+
(
|
| 6653 |
+
'Use DeepSeek to rewrite the passage without external literature if this is a citation. If it is instead a '
|
| 6654 |
+
'product, method, model, software, or ordinary year label, preserve the factual content and clarify the wording. '
|
| 6655 |
+
'Do not terminate manuscript generation solely because this soft ambiguity remains after bounded repair.'
|
| 6656 |
+
),
|
| 6657 |
+
)
|
| 6658 |
+
|
| 6659 |
+
return rows
|
| 6660 |
def audit_and_resolve_citations(
|
| 6661 |
self, text, section_name='', section_id='', quarantine=False, allowed_tags=None
|
| 6662 |
):
|
|
|
|
| 6666 |
)
|
| 6667 |
value = self.strip_ineligible_current_study_citations(str(text or ''), section_name)
|
| 6668 |
if quarantine:
|
| 6669 |
+
quarantine_rows = self._quarantined_unit_citation_violations(value)
|
| 6670 |
+
if ownership_rows or quarantine_rows:
|
| 6671 |
+
# Do not silently delete a citation-dependent claim. The owning
|
| 6672 |
+
# Section retry/adaptive-repair path receives a typed violation
|
| 6673 |
+
# and must rewrite the unit without external literature.
|
| 6674 |
+
self._last_citation_semantic_violations = (
|
| 6675 |
+
list(ownership_rows) + list(quarantine_rows)
|
| 6676 |
+
)
|
| 6677 |
+
for row in quarantine_rows:
|
| 6678 |
+
self._append_citation_audit(
|
| 6679 |
+
section_name, 'reject_citation_in_quarantined_unit',
|
| 6680 |
+
str(row.get('citation') or ''), '', str(row.get('reason') or '')
|
| 6681 |
+
)
|
| 6682 |
+
return value
|
| 6683 |
+
return value
|
| 6684 |
+
|
| 6685 |
+
# P3-A4: collect deterministic citation-authority violations separately.
|
| 6686 |
+
# semantic_audit_claim_specific_citations() owns its own last-violation
|
| 6687 |
+
# buffer, so these rows are merged back only after that audit completes.
|
| 6688 |
+
authority_rows = []
|
| 6689 |
unknown = sorted({f"[{tag.upper()}]" for tag in re.findall(r'CITE-\d+', value, re.I)} - set(self.citation_map))
|
| 6690 |
if unknown:
|
| 6691 |
for tag in unknown:
|
| 6692 |
+
authority_rows.append({
|
| 6693 |
+
'type': 'citation_unknown_tag',
|
| 6694 |
+
'tag': tag,
|
| 6695 |
+
'sentence': tag,
|
| 6696 |
+
'reason': 'The manuscript unit emitted a citation capability that does not exist in authoritative citation_map.',
|
| 6697 |
+
'repair_instruction': (
|
| 6698 |
+
'Use only citation tags frozen for this manuscript unit, or narrow/rewrite the claim so no unsupported citation is needed.'
|
| 6699 |
+
),
|
| 6700 |
+
})
|
| 6701 |
value = re.sub(rf'\s*\[?{re.escape(tag.strip("[]"))}\]?\s*', ' ', value, flags=re.I)
|
| 6702 |
+
self._append_citation_audit(section_name, 'reject_unknown_tags_for_section_retry', text, value, ','.join(unknown))
|
| 6703 |
+
|
| 6704 |
allowed = None if allowed_tags is None else {
|
| 6705 |
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 6706 |
}
|
|
|
|
| 6720 |
self._append_citation_audit(
|
| 6721 |
section_name, 'reject_outside_frozen_section_whitelist', text, value, tag
|
| 6722 |
)
|
| 6723 |
+
|
| 6724 |
+
# P3-A4 closes the resolved-marker bypass before section sealing. Exact,
|
| 6725 |
+
# unambiguous, current-unit-authorized markers are normalized back to raw
|
| 6726 |
+
# capabilities so the existing claim-specific semantic audit still owns
|
| 6727 |
+
# claim/evidence adjudication. All other forms become typed violations.
|
| 6728 |
+
value, resolved_authority_rows = self._normalize_pre_resolved_citation_authority(
|
| 6729 |
+
value, allowed_tags=allowed, section_name=section_name
|
| 6730 |
+
)
|
| 6731 |
+
authority_rows.extend(resolved_authority_rows)
|
| 6732 |
+
|
| 6733 |
has_citations = bool(re.search(r'CITE-\d+', value, re.I))
|
| 6734 |
violations = self.find_multi_citation_violations(value)
|
| 6735 |
if has_citations and self.citation_semantic_audit.get('enabled', True):
|
|
|
|
| 6760 |
value = self._local_citation_recovery(value, section_name, retain_lexical=False)
|
| 6761 |
elif violations:
|
| 6762 |
value = self._local_citation_recovery(value, section_name)
|
| 6763 |
+
|
| 6764 |
+
pre_semantic_rows = list(ownership_rows) + list(outside_rows) + list(authority_rows)
|
| 6765 |
+
if pre_semantic_rows:
|
| 6766 |
self._last_citation_semantic_violations = (
|
| 6767 |
+
pre_semantic_rows + list(self._last_citation_semantic_violations or [])
|
|
|
|
|
|
|
| 6768 |
)
|
| 6769 |
+
|
| 6770 |
+
# P3-A4 reliability rule: once deterministic identity/scope authority has
|
| 6771 |
+
# already rejected this attempt, return it to the owning bounded Section
|
| 6772 |
+
# retry instead of letting the legacy multi-source hard exception preempt
|
| 6773 |
+
# typed recovery. The rejected bytes can never be sealed because runner
|
| 6774 |
+
# treats these rows as a hard pre-seal invariant.
|
| 6775 |
+
if outside_rows or authority_rows:
|
| 6776 |
+
return value
|
| 6777 |
+
|
| 6778 |
if self.find_multi_citation_violations(value):
|
| 6779 |
value = self._local_citation_recovery(value, section_name)
|
| 6780 |
if self.find_multi_citation_violations(value):
|