File size: 8,845 Bytes
7ed86c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
"""Stage 8: write README.md + stats.md into the output directory."""
import json
import os
from collections import defaultdict

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUTDIR = os.path.expandvars(os.environ.get("FOLD_OUTDIR", "$STOICHEIA_DATA"))
W = os.path.join(ROOT, "work")


def load(name):
    p = os.path.join(W, name)
    if os.path.exists(p):
        with open(p) as f:
            return json.load(f)
    return {}


s1, s2, s3 = load("stage1_stats.json"), load("stage2_stats.json"), load("stage3_stats.json")
s4, s5, s6 = load("stage4_stats.json"), load("stage5_stats.json"), load("stage6_stats.json")
s3b, s11 = load("stage3b_stats.json"), load("stage11_stats.json")
dc, s12 = load("doublecheck_report.json"), load("stage12_stats.json")
ver = load("verify_report.json")

TIERS = ["pristine", "repaired", "bronze", "inscriptions"]

lines = ["# 10-fold split — statistics", ""]

lines += ["## Verification (independent re-check of materialized folds)", "",
          "| fold | train vs val+test 8-gram hits | sentence hits | val vs test 8-gram | sentence | status |",
          "|---|---|---|---|---|---|"]
for k in range(10):
    v = ver.get("fold_%d" % k)
    if not v:
        lines.append("| %d | – | – | – | – | not verified |" % k)
        continue
    t, vv = v["train_vs_valtest"], v["val_vs_test"]
    lines.append("| %d | %d | %d | %d | %d | %s |" % (
        k, t["gram_hits"], t["sent_hits"], vv["gram_hits"], vv["sent_hits"],
        "PASS" if v["PASS"] else "**FAIL**"))
lines.append("")

if s6:
    lines += ["## Per-fold output (records / Mchars emitted; Mchars excised; records dropped)", ""]
    agg = defaultdict(lambda: defaultdict(lambda: [0, 0, 0, 0]))
    for key, v in s6.items():
        k, split, tier = key.split("|")
        a = agg[int(k)][(split, tier)]
        for j in range(4):
            a[j] += v[j]
    for k in range(10):
        lines.append("### fold %d" % k)
        lines.append("| split | tier | records | Mchars | Mchars excised | dropped records |")
        lines.append("|---|---|---|---|---|---|")
        for split in ("train", "val", "test"):
            for tier in TIERS:
                r = agg[k].get((split, tier))
                if not r:
                    continue
                lines.append("| %s | %s | %d | %.1f | %.1f | %d |" % (
                    split, tier, r[0], r[1] / 1e6, r[2] / 1e6, r[3]))
        lines.append("")

if dc:
    lines += ["## Adversarial double-check", "",
              "- D1 mutation test (verifier detects planted verbatim AND"
              " orthographically-mutated editions; flags nothing on genuine"
              " train): **%s**" % ("PASS" if dc.get("D1_mutation", {}).get("PASS") else "FAIL"),
              "- D2 TM/PHI digit-rule compliance + D3 id-disjointness per fold: **%s**"
              % ("PASS" if all(v.get("PASS") for k, v in dc.items()
                               if k.startswith("fold_")) else "FAIL"), ""]
if s11:
    tot = sum(v["dropped_bronze"] for v in s11.values()) // max(1, len(s11))
    lines += ["## Greek-origin bronze exclusion (anti-paraphrase guard)", "",
              "~%d bronze records (back-translations of Latin translations of"
              " GREEK works: Graeca miscellanea, Biblia, Ptolemaeus Latinus,"
              " Versiones latinae, Greek-author works in PL etc.) removed from"
              " every fold's train." % tot, ""]
if s12:
    lines += ["## Rare-word paraphrase screen (per-fold bronze filter)", "",
              "| fold | bronze dropped | Mchars |", "|---|---|---|"]
    for k in range(10):
        v = s12.get("fold_%d" % k, {})
        lines.append("| %d | %s | %s |" % (k, v.get("dropped_bronze", "–"),
                                           v.get("dropped_Mchars", "–")))
    lines.append("")

for name, s in [("Stage 1 (tokenization)", s1), ("Stage 2 (pristine clustering)", s2),
                ("Stage 3 (bucket packing)", s3), ("Stage 3b (canonical zones)", s3b),
                ("Stage 4 (contamination index)", s4),
                ("Stage 5 (conflict masks)", s5)]:
    lines += ["## " + name, "", "```json", json.dumps(s, indent=2), "```", ""]

with open(os.path.join(OUTDIR, "stats.md"), "w") as f:
    f.write("\n".join(lines))

readme = """# 10-fold leakage-proof split of the Ancient Greek corpora

Built from:
- `anonymous-stoicheia/AncientGreek` (pristine + repaired tiers)
- `anonymous-stoicheia/SyntheticAncientGreek-CorpusCorporum` (bronze)
- `anonymous-stoicheia/Inscriptions_2` (PHI inscriptions + GPT-4o synthetic variants)

## Layout
`fold_k/{train,val,test}.jsonl.zst` for k = 0..9.

Records: `{"id", "tier", "source", "text"}`; excised train records are split
into `id#segN` segments (maximal runs of consecutive clean sentences).
Val/test inscriptions instead carry the full original PHI row **minus** the
synthetic fields.

## Fold design
- Literary pristine was clustered (same text in multiple editions -> one
  cluster: id-prefix, exact-duplicate, MinHash-LSH and shared-sentence
  evidence) and clusters packed into 10 buckets of ~10% of words each.
  Fold k: test = bucket k, val = bucket (k+1) mod 10, train = the rest.
- Papyri (ddbdp via TM number, dclp) and PHI inscriptions use the fixed
  Ithaca-compatible digit rule in every fold: number ends in 3 -> test,
  4 -> val, else train. Synthetic variants of a val/test PHI number appear
  nowhere.
- repaired + bronze are train-only. A repaired record additionally inherits
  the ZONE of its pristine sibling volume/work (same id prefix) or, for
  papyri, its TM digit -- so OCR-damaged copies and sibling pages of a
  val/test document are excluded from that fold's train entirely, not just
  excised sentence-wise.
- Bronze passages that are back-translations of Latin translations of
  GREEK-origin works are removed from train in every fold: they are machine
  paraphrases of real Greek texts and would be invisible to verbatim
  matching. Two layers: (1) catalog flags -- translation corpora (Graeca
  miscellanea, Biblia, Ptolemaeus Latinus, Versiones latinae), Greek-writing
  authors, Greek author names or translation markers in the TITLE (catches
  e.g. "Iliados liber XIV Latine redditus" by 'Anonymus'); (2) an empirical
  rare-word screen per fold -- any remaining bronze record sharing >= 4 rare
  words (test-side document frequency <= 3, length >= 6) with a single test
  record is dropped from that fold's train, deliberately over-excluding
  topical coincidences. The remaining bronze derives from genuinely Latin
  works, whose Greek is model-generated language with no Greek source text
  to memorize.

## Intended claim (for reviewers)
A model trained on fold k's train set has never seen the test texts of fold
k in any form: not the same edition, not another edition, not OCR-damaged or
LLM-repaired copies, not reordered or unpunctuated variants, not quotations
of 8+ words, not whole sentences of any length, not GPT-4o synthetic
variants of test inscriptions, and not machine back-translations of Latin
translations of Greek test works. Conjectures the model produces on test
texts are therefore grounded in the rest of Greek literature, not in
memorization of the target text. Residual channels that remain by design:
sub-8-word formulaic phrases (shared language, not text identity), human
paraphrase within Greek literature itself (scholia, indirect tradition --
the same evidence a human conjecturer legitimately uses), and other works
by the same author.

## Decontamination (100% strict, no frequency exemptions)
All text is normalized to a matching skeleton (NFD, diacritics stripped,
lowercase, sigma-folded, non-Greek-letters removed) and sentence-tokenized.
A train sentence is EXCISED when, against any zone that is val/test in the
fold, it has (a) an identical skeleton, (b) an identical sorted-word bag, or
(c) any shared word-8-gram (computed over cross-sentence word streams), or
(d) its record matches a val/test-eligible pristine record at document level
(MinHash est. Jaccard >= 0.5, both records >= 35 words, sizes within 3x). Remaining consecutive sentences are stitched
into segments (>= 100 chars, train; >= 25 chars, val). Val is additionally
cleaned against test the same way.

Guarantee (verified independently per fold, see stats.md): train shares no
8-word verbatim sequence (after normalization) and no complete sentence with
val or test; val shares none with test. Sub-8-word fragments inside longer
differing sentences are not treated as leakage. Bronze is machine
back-translation from Latin; verbatim quotations are caught, free paraphrase
of content is out of scope by design.

Pipeline: $STOICHEIA_ROOT/pipeline/
"""
with open(os.path.join(OUTDIR, "README.md"), "w") as f:
    f.write(readme)
print("wrote", os.path.join(OUTDIR, "README.md"), "and stats.md")