Spaces:
Paused
Paused
Stage_V_0 Path D serve: upload training/citation_utils_v5.py
Browse files- training/citation_utils_v5.py +125 -0
training/citation_utils_v5.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multiplicity-preserving citation guard — v5 wrapper layer.
|
| 2 |
+
|
| 3 |
+
Spec: ``humanizer-v5-detector-in-the-loop`` (R9.3, R11, R13.1).
|
| 4 |
+
|
| 5 |
+
Why this module exists
|
| 6 |
+
----------------------
|
| 7 |
+
R9.3 / design Property 3 require the Citation_Preservation_Guard to be
|
| 8 |
+
*multiplicity-preserving*: a citation pattern that appears ``N`` times in the
|
| 9 |
+
input MUST appear at least ``N`` times in the output, otherwise the check fails.
|
| 10 |
+
"Single-instance preservation MUST NOT be accepted as multi-instance
|
| 11 |
+
preservation."
|
| 12 |
+
|
| 13 |
+
The shipped predicate
|
| 14 |
+
:func:`training.data.citation_utils.passes_citation_check` does **not** enforce
|
| 15 |
+
this. Its ``missing_citations`` helper contains a substring fallback
|
| 16 |
+
(``if citation in ai_text: continue``) that only flags a citation as missing
|
| 17 |
+
when its output count drops to **zero**. Consequently a reduction such as
|
| 18 |
+
``(Müller 2019)`` appearing 2× in the input but 1× in the output is wrongly
|
| 19 |
+
accepted (returns ``True`` where R9.3 requires ``False``). This was confirmed by
|
| 20 |
+
property test P3.
|
| 21 |
+
|
| 22 |
+
``training/data/citation_utils.py`` is R11/R13.1-protected and MUST NOT be
|
| 23 |
+
modified. This module is therefore a **thin wrapper** that reuses
|
| 24 |
+
:func:`extract_citations` (import-only — the multiplicity-correct primitive) and
|
| 25 |
+
re-derives the predicate with strict per-citation count comparison via
|
| 26 |
+
:class:`collections.Counter`. No logic is copied from the protected module and
|
| 27 |
+
the protected module is left byte-for-byte unchanged.
|
| 28 |
+
|
| 29 |
+
Exports
|
| 30 |
+
-------
|
| 31 |
+
- ``passes_citation_check_v5``: strict, multiplicity-preserving predicate
|
| 32 |
+
(drop-in replacement for ``passes_citation_check`` with an identical
|
| 33 |
+
``(human_text, ai_text) -> bool`` contract).
|
| 34 |
+
- ``missing_citations_v5``: citations under-represented in the output, with
|
| 35 |
+
multiplicity, for diagnostics/logging.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
from __future__ import annotations
|
| 39 |
+
|
| 40 |
+
from collections import Counter
|
| 41 |
+
|
| 42 |
+
# Import-only reuse of the multiplicity-correct extraction primitive from the
|
| 43 |
+
# R11/R13.1-protected module. We deliberately do NOT import
|
| 44 |
+
# ``passes_citation_check`` / ``missing_citations`` (the non-multiplicity-aware
|
| 45 |
+
# predicates) — only the raw ``extract_citations`` building block.
|
| 46 |
+
from training.data.citation_utils import extract_citations
|
| 47 |
+
|
| 48 |
+
__all__ = [
|
| 49 |
+
"missing_citations_v5",
|
| 50 |
+
"passes_citation_check_v5",
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def missing_citations_v5(human_text: str, ai_text: str) -> list[str]:
|
| 55 |
+
"""Return citations under-represented in *ai_text*, counted with multiplicity.
|
| 56 |
+
|
| 57 |
+
A citation is "under-represented" when it appears fewer times in *ai_text*
|
| 58 |
+
than in *human_text*. The shortfall is reported with multiplicity: if a
|
| 59 |
+
citation occurs 3× in *human_text* and 1× in *ai_text*, it appears twice in
|
| 60 |
+
the result (the two unmatched occurrences). Output order follows the order
|
| 61 |
+
of first appearance in *human_text*.
|
| 62 |
+
|
| 63 |
+
Unlike :func:`training.data.citation_utils.missing_citations`, there is no
|
| 64 |
+
substring fallback: presence is measured strictly by *count* of regex
|
| 65 |
+
matches, so a reduced-but-present citation is correctly reported as missing.
|
| 66 |
+
|
| 67 |
+
Parameters
|
| 68 |
+
----------
|
| 69 |
+
human_text:
|
| 70 |
+
Source text whose citations must all be reproduced.
|
| 71 |
+
ai_text:
|
| 72 |
+
Candidate text that should preserve every citation from *human_text*
|
| 73 |
+
with at least the same multiplicity.
|
| 74 |
+
|
| 75 |
+
Returns
|
| 76 |
+
-------
|
| 77 |
+
list[str]
|
| 78 |
+
Citations missing from *ai_text*, with multiplicity. Empty when every
|
| 79 |
+
citation appears in *ai_text* at least as many times as in *human_text*.
|
| 80 |
+
"""
|
| 81 |
+
human_counts = Counter(extract_citations(human_text))
|
| 82 |
+
if not human_counts:
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
ai_counts = Counter(extract_citations(ai_text))
|
| 86 |
+
|
| 87 |
+
missing: list[str] = []
|
| 88 |
+
# Preserve first-appearance order from the human text for stable diagnostics.
|
| 89 |
+
for citation in extract_citations(human_text):
|
| 90 |
+
shortfall = human_counts[citation] - ai_counts.get(citation, 0)
|
| 91 |
+
if shortfall > 0:
|
| 92 |
+
missing.append(citation)
|
| 93 |
+
# Decrement so each of the `shortfall` unmatched occurrences is
|
| 94 |
+
# reported exactly once as we walk the human-text occurrences.
|
| 95 |
+
human_counts[citation] -= 1
|
| 96 |
+
ai_counts[citation] = ai_counts.get(citation, 0) # no-op clarity
|
| 97 |
+
return missing
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def passes_citation_check_v5(human_text: str, ai_text: str) -> bool:
|
| 101 |
+
"""Return ``True`` iff every citation's count in *ai_text* is >= its count in *human_text*.
|
| 102 |
+
|
| 103 |
+
This is the strict, multiplicity-preserving form of the
|
| 104 |
+
Citation_Preservation_Guard required by R9.3 / design Property 3. It is a
|
| 105 |
+
drop-in replacement for
|
| 106 |
+
:func:`training.data.citation_utils.passes_citation_check`: same
|
| 107 |
+
``(human_text, ai_text) -> bool`` signature and accept/reject contract, but
|
| 108 |
+
it rejects records where any citation's multiplicity is reduced — including
|
| 109 |
+
the reduced-but-present case (e.g. 2× → 1×) that the original wrongly
|
| 110 |
+
accepts.
|
| 111 |
+
|
| 112 |
+
Parameters
|
| 113 |
+
----------
|
| 114 |
+
human_text:
|
| 115 |
+
Source text whose citations must all be preserved.
|
| 116 |
+
ai_text:
|
| 117 |
+
Candidate AI paraphrase.
|
| 118 |
+
|
| 119 |
+
Returns
|
| 120 |
+
-------
|
| 121 |
+
bool
|
| 122 |
+
``True`` when no citation is under-represented in *ai_text*, ``False``
|
| 123 |
+
otherwise. An increased count in *ai_text* still passes.
|
| 124 |
+
"""
|
| 125 |
+
return not missing_citations_v5(human_text, ai_text)
|