LevArtesa commited on
Commit
72e704c
·
verified ·
1 Parent(s): e83c8d9

Stage_V_0 Path D serve: upload training/citation_utils_v5.py

Browse files
Files changed (1) hide show
  1. 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)