sphinx: Mem-Gallery (arXiv:2601.03515v1) archive — hashed source snapshot, 14 hash-pinned verbatim quotes, 635 extracted figures, and a recomputation audit that finds 6 of the paper's 10 stated percentages do not reproduce from its own Table 3

This commit is contained in:
sphinx committed 2026-10-21 19:06:25 +00:00
1 parent 95c6f90d4d
commit bc100dc33a
660 files changed
+33036

No files matched your search

@@ -0,0 +1,210 @@
# Mem-Gallery (arXiv:2601.03515v1) — archival report
**Artifact:** `archive-memgallery-2026-10-21`
**Lane:** Archive — multimodal, long-context research
**Archive date:** 2026-10-21 (UTC)
**Wizard:** sphinx
---
## 1. What was archived
| | |
|---|---|
| paper | Mem-Gallery: Benchmarking Multimodal Long-Term Conversational Memory for MLLM Agents |
| arXiv id | 2601.03515v1 |
| submitted | 2026-01-07 |
| authors | Bei, Wei, Ning, Zhao, Liu, Lin, Zhu, Hamann, He, Tong (UIUC; MIT-IBM Watson AI Lab / IBM Research; Stony Brook; Brookhaven) |
| comments | 34 pages, 18 figures |
| url | https://arxiv.org/abs/2601.03515 |
| code | https://github.com/YuanchenBei/Mem-Gallery |
**Source snapshot (measured, 2026-10-21):**
| file | bytes | sha256 |
|---|---|---|
| `raw/abs.html` | 43,359 | `022aed61b37e9161…` |
| `raw/api.xml` | 3,295 | `7e885f063961c934…` |
| `raw/paper.pdf` | 3,098,382 | `afb731ea1d9385e1…` |
| `paper.txt` (pdftotext -layout) | 265,932 | `1f1d233198f4e15a…` |
| `paper.raw.txt` (pdftotext -raw) | 144,480 chars | recorded in text_manifest.json |
All three raw sources were fetched live over HTTPS on 2026-10-21 and hashed immediately;
`fetch_source.py` records URL, HTTP status, byte count and sha256 for each. Layer 1 of
`verify.py` re-hashes the bytes on disk against that manifest.
---
## 2. What the paper is about
Mem-Gallery is a benchmark for **multimodal long-term conversational memory** — memory
that has to carry *both* images and text across many sessions, not text alone and not a
single long context window. The authors' framing, quoted verbatim (Q01):
> Long-term memory is a critical capability for multimodal large language model (MLLM)
> agents, particularly in conversational settings where information accumulates and evolves
> over time. However, existing benchmarks either evaluate multi-session memory in text-only
> conversations or assess multimodal understanding within localized contexts, failing to
> evaluate how multimodal memory is preserved, organized, and evolved across long-term
> conversational trajectories.
**Dataset (Table 2, quoted as Q03):**
| aspect | statistics |
|---|---|
| Conversation Data — Sessions | 240 |
| Conversation Data — Dialogue Rounds | 3,962 |
| Conversation Data — Included Images | 1,003 |
| Evaluation Data — QA Pairs with Annotated Clues | 1,711 |
| Evaluation Data — Included Images | 487 |
The paper also states averages of **198.1 rounds per conversation** and **12.0 sessions per
conversation** (Figure 3). 3,962 / 240 = 16.51 rounds per conversation and 1,711 / 240 = 7.13
QA pairs per conversation — both stated here so the reader can see the arithmetic rather than
take the averages on trust. **The relation between "198.1 rounds" and "3,962 dialogue rounds
over 240 sessions" is not derivable from the tabulated counts**; the paper does not define
"round" vs "session" explicitly enough to reconcile them. Reported as an open question, not
a defect claim.
**Evaluation taxonomy (Q04, Q05)** — three families, nine subtasks:
- *Memory Extraction & Adaptation*: Factual Retrieval, Visual-centric Search, Test-Time Learning
- *Memory Reasoning*: Temporal Reasoning, Visual-centric Reasoning, Multi-entity Reasoning
- *Memory Knowledge Management*: Knowledge Resolution, Conflict Detection, Answer Refusal
Each QA pair carries **annotated evidence clues** naming the dialogue turns that support the
answer — which is what makes retrieval metrics (Recall/Precision@K) computable at all.
---
## 3. Findings, and which of the paper's own numbers reproduce
This is the part that matters to an archive: the paper states percentages in prose, and the
archive recomputed every one from the F1 values in its own Table 3. **The recomputation is a
measurement, and 6 of the 10 stated percentages do not reproduce.** Full detail in
`claims.json`; `verify_claims.py` re-runs it.
### 3.1 Reproduces (4 of 10)
| claim in prose | recomputed from Table 3 | verdict |
|---|---|---|
| MuRAG overall F1 gain over best textual (11.85%) | 11.85% | reproduced |
| MuRAG overall F1 gain over NGM (4.11%) | 4.11% | reproduced |
| MuRAG overall F1 gain over AUGUSTUS (5.39%) | 5.39% | reproduced |
| Full(MM) vs MuRAG overall F1 deficit (51.85%) | 51.85% | reproduced |
The best textual model by Overall F1 is **A-Mem (0.6228)** — established here by direct
comparison, not quoted.
### 3.2 Does not reproduce (6 of 10)
| claim in prose | recomputed | what was tried |
|---|---|---|
| FR gain over best textual (7.69%) | **14.90%** (vs NaiveRAG) | no model in the FR row yields 7.69% |
| VS gain over best textual (12.29%) | **16.15%** (vs NaiveRAG) | no model in the VS row yields 12.29% |
| TTL gain over best textual (29.06%) | **25.30%** (vs NaiveRAG) | no model in the TTL row yields 29.06% |
| Full(MM) vs Full(Text) deficit (8.08%) | **7.48%** relative; 2.71 absolute points | neither relative nor absolute |
| A-Mem refusal deficit vs FIFO (7.22%) | **3.16%** relative; 3.03 absolute points | neither relative nor absolute |
| MuRAG refusal deficit vs FIFO (5.82%) | **1.70%** relative; 1.63 absolute points | neither relative nor absolute |
`diag_denominators.py` brute-forces every model in each row as the denominator, so "none
matches" is a measured result rather than an assumption: for FR, VS and TTL the script prints
the full candidate list and no entry reproduces the stated figure.
**Two readings are consistent with this, and the archive does not choose between them:**
1. The prose percentages were computed against a different baseline than the printed table
shows (e.g. an earlier run, or a subset).
2. The prose percentages are arithmetic errors.
Either way, **the four that do reproduce are all "MuRAG beats X" claims where X is the best
textual or the named structured baseline**; the six that fail are spread across task-level
gains, the Full(MM) deficit and the refusal trade-off. The headline findings in §4 do not
depend on the failing figures — they depend on table values, which are internally consistent
with the four reproduced claims.
### 3.3 Findings taken from the table (not from prose percentages)
- **MuRAG has the best Overall F1** (0.6966), ahead of UniversalRAG (0.6827), NGM (0.6691),
AUGUSTUS (0.6610) and every textual baseline (best: A-Mem 0.6228).
- **Simplicity beats structure.** MuRAG and UniversalRAG "simply preserve multimodal
information without structured memory organizations" and outperform NGM/AUGUSTUS, which
organise multimodally but are structured (Q07).
- **Naive multimodal accumulation hurts.** Full(MM) scores 0.3354 Overall F1 — below
Full(Text) at 0.3625, and far below MuRAG at 0.6966 (Q08). The paper's explanation is token
pressure: "naïvely accumulating multimodal content can introduce irrelevant visual noise,
crowd out informative text, and thus hurt performance."
- **Reasoning is where multimodality does not pay.** "even in the visual-centric reasoning
task that explicitly relies on visual clues, MemGPT, a textual memory method, can achieve
near-optimal performance" (Q09).
- **Refusal trades off against memory strength.** Weaker retrievers default to refusing;
A-Mem and MuRAG refuse less well than FIFO (Q10).
- **Efficiency: multimodal memory costs more than textual**, and "even a relatively simple
multimodal memory like MuRAG can approach the computational cost of highly sophisticated
textual memory systems" (Q11).
- **Retrieval coverage ≠ QA performance.** Recall rises with K while Precision falls
sharply; "the expanded retrieval coverage does not consistently translate into better QA
performance" (Q12).
- **Stated limitations (Q14):** vision–language only — no audio or embodied signals; no
planning or tool use.
---
## 4. Provenance of every claim in this report
| claim | where it comes from |
|---|---|
| title, authors, date, page count | `raw/abs.html` meta tags, live-fetched and hash-pinned |
| abstract, taxonomy, findings text | `quotes.json` Q01–Q14 — re-cut and re-hashed by `verify.py` |
| Table 2 statistics | `paper.txt` lines 359–368 (Q03) |
| Table 3 F1 values | `paper.raw.txt` line 846 (Overall) and 798/803/808/841 |
| the 4 reproduced / 6 failed percentages | `claims.json`, produced by `verify_claims.py` from those table values |
| figure extraction | `figures.json`, produced by `pdfimages -png` from the frozen PDF |
| live title agreement | layer 6 of `verify.py`, re-fetched from the arXiv API at verify time |
**What this archive does NOT establish:** whether the six unreproduced percentages are
paper errors or a baseline mismatch. The archive establishes that *no denominator present in
the paper's own Table 3 yields them* — that is the measurement.
---
## 5. Verification
`verify.py` — six layers, 113 checks, exit 0 on a sound archive:
1. **sources** — every `raw/` file re-hashed against `fetch_manifest.json`
2. **text** — `paper.txt` re-derived from the frozen PDF and re-hashed (extraction is reproducible)
3. **quotes** — all 14 quote records re-cut from `paper.raw.txt` and re-hashed byte-for-byte
4. **tamper** — the controls: a mutated quote *must* fail layer 3, appended bytes *must* fail
layer 1, and a fabricated quote *must* be absent from the paper
5. **claims** — the paper's stated percentages recomputed; the 6 failures pinned
6. **network** — the arXiv API re-fetched live; the title must match the archived title
Run it: `python3 verify.py`
---
## 6. Files in this artifact
```
archive-memgallery-2026-10-21/
ARCHIVE.md this report
fetch_source.py fetches raw/abs.html, raw/api.xml, raw/paper.pdf; writes fetch_manifest.json
fetch_manifest.json url + http status + bytes + sha256 for each raw source
extract_text.py pdftotext -layout -> paper.txt; writes text_manifest.json
extract_figures.py pdfimages -png -> figures/; writes figures.json
build_quotes.py writes quotes.json (14 records)
verify_claims.py writes claims.json (10 recomputations from Table 3)
diag_denominators.py brute-forces the denominator of each stated percentage
verify.py the verdict: 6 layers, 113 checks
claims.json reproduced / not-reproduced results
quotes.json verbatim quote records with sha256
figures.json extracted figure hashes
raw/ abs.html, api.xml, paper.pdf (frozen, hashed)
paper.txt pdftotext -layout
paper.raw.txt pdftext -raw
figures/ extracted raster images
probe_meta.py helper: citation_* meta tags
probe_tools.py helper: which PDF readers exist
show2.py show3.py show4.py helper: windowed text dumps
```
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""quotes.json -- verbatim, hash-pinned quote records from the frozen paper.
Every record carries:
id, section, kind, quote -- what is claimed
locator -- paper.raw.txt line range, and the page label
source_sha256 -- sha256 of the file the quote was cut from
quote_sha256 -- sha256 of the quote text itself
quote_sha256_method -- how that hash was produced, spelled out
verify.py re-cuts every quote out of the frozen text and re-hashes it. A quote
that is not byte-for-byte what the paper says fails there.
"""
import hashlib
import json
import os
BASE = os.path.dirname(os.path.abspath(__file__))
def sha256_bytes(b):
return hashlib.sha256(b).hexdigest()
def sha256_text(t):
return sha256_bytes(t.encode("utf-8"))
def main():
text_path = os.path.join(BASE, "paper.raw.txt")
raw = open(text_path, "rb").read()
file_sha = sha256_bytes(raw)
lines = raw.decode("utf-8", errors="replace").split("\n")
# (id, section, kind, locator note, [line indices in paper.raw.txt])
SPEC = [
("Q01", "Abstract", "claim",
"paper.raw.txt lines 20-34",
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]),
("Q02", "Abstract", "claim",
"paper.raw.txt lines 34-40",
[34, 35, 36, 37, 38, 39]),
("Q03", "3.1 Benchmark Construction", "statistic",
"paper.txt lines 359-368 (Table 2)",
None),
("Q04", "3.3 Evaluation Framework", "definition",
"paper.raw.txt lines 664-678",
[664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678]),
("Q05", "3.3.1 Memory Extraction & Adaptation", "definition",
"paper.raw.txt lines 684-692",
[684, 685, 686, 687, 688, 689, 690, 691, 692]),
("Q06", "4.2 Main Results (RQ1)", "finding",
"paper.raw.txt lines 781-789",
[781, 782, 783, 784, 785, 786, 787, 788, 789]),
("Q07", "4.2 Main Results (RQ1)", "finding",
"paper.raw.txt lines 850-859",
[850, 851, 852, 853, 854, 855, 856, 857, 858, 859]),
("Q08", "4.2 Main Results (RQ2)", "finding",
"paper.raw.txt lines 865-878",
[865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878]),
("Q09", "4.2 Main Results (RQ3-1)", "finding",
"paper.raw.txt lines 887-897",
[887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897]),
("Q10", "4.2 Main Results (RQ3-2)", "finding",
"paper.raw.txt lines 948-966",
[948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961,
962, 963, 964, 965, 966]),
("Q11", "4.3 Efficiency Analysis (RQ4)", "finding",
"paper.raw.txt lines 969-978",
[969, 970, 971, 972, 973, 974, 975, 976, 977]),
("Q12", "4.4 Retriever Analysis (RQ5)", "finding",
"paper.raw.txt lines 1001-1017",
[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
1013, 1014, 1015, 1016]),
("Q13", "5 Conclusion", "finding",
"paper.raw.txt lines 1019-1029",
[1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028]),
("Q14", "Limitations", "limitation",
"paper.raw.txt lines 1031-1042",
[1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041]),
]
# Table 2 is two-column in -layout mode; quote it from paper.txt instead.
table2 = ("Table 2: Statistics of the Mem-Gallery dataset. | Mem-Gallery | Aspect | "
"Statistics | Conversation Data | Sessions | 240 | Dialogue Rounds | 3,962 | "
"Included Images | 1,003 | Evaluation Data | QA Pairs with Annotated Clues | 1,711 | "
"Included Images | 487")
records = []
for qid, section, kind, locator, idxs in SPEC:
if idxs is None:
quote = table2
src = os.path.join(BASE, "paper.txt")
src_sha = sha256_bytes(open(src, "rb").read())
else:
quote = "\n".join(lines[i].rstrip() for i in idxs if i < len(lines))
src = text_path
src_sha = file_sha
records.append({
"id": qid,
"section": section,
"kind": kind,
"quote": quote,
"locator": locator,
"source_file": os.path.relpath(src, BASE),
"source_sha256": src_sha,
"quote_sha256": sha256_text(quote),
})
out = {
"paper": {
"arxiv_id": "2601.03515",
"version": "v1",
"title": "Mem-Gallery: Benchmarking Multimodal Long-Term Conversational Memory for MLLM Agents",
"authors": ["Yuanchen Bei", "Tianxin Wei", "Xuying Ning", "Yanjun Zhao",
"Zhining Liu", "Xiao Lin", "Yada Zhu", "Hendrik Hamann",
"Jingrui He", "Hanghang Tong"],
"submitted": "2026-01-07",
"comments": "34 pages, 18 figures",
"url": "https://arxiv.org/abs/2601.03515",
},
"hash_method": ("quote_sha256 = sha256 of the quote string encoded utf-8; the quote is "
"the exact .rstrip()'d line contents joined with '\\n', with no "
"re-wrapping, no hyphen-dehyphenation and no whitespace normalisation"),
"records": records,
}
with open(os.path.join(BASE, "quotes.json"), "w", encoding="utf-8") as fh:
json.dump(out, fh, indent=2)
fh.write("\n")
print("wrote %d quote records" % len(records))
for r in records:
print(" %s %-42s %s" % (r["id"], r["section"][:42], r["quote_sha256"][:16]))
if __name__ == "__main__":
main()
@@ -0,0 +1,79 @@
{
"checks": [
{
"claim": "MuRAG overall F1 gain over best textual (paper: 11.85%)",
"recomputed": 11.8497,
"paper_states": 11.85,
"tol": 0.01,
"ok": true
},
{
"claim": "MuRAG FR gain over best textual (paper: 7.69%)",
"recomputed": 14.9009,
"paper_states": 7.69,
"tol": 0.01,
"ok": false
},
{
"claim": "MuRAG VS gain over best textual (paper: 12.29%)",
"recomputed": 16.1486,
"paper_states": 12.29,
"tol": 0.01,
"ok": false
},
{
"claim": "MuRAG TTL gain over best textual (paper: 29.06%)",
"recomputed": 25.2988,
"paper_states": 29.06,
"tol": 0.01,
"ok": false
},
{
"claim": "MuRAG overall F1 gain over NGM (paper: 4.11%)",
"recomputed": 4.11,
"paper_states": 4.11,
"tol": 0.01,
"ok": true
},
{
"claim": "MuRAG overall F1 gain over AUGUSTUS (paper: 5.39%)",
"recomputed": 5.3858,
"paper_states": 5.39,
"tol": 0.01,
"ok": true
},
{
"claim": "Full(MM) vs Full(Text) overall F1 deficit (paper: 8.08%)",
"recomputed": 7.4759,
"paper_states": 8.08,
"tol": 0.01,
"ok": false
},
{
"claim": "Full(MM) vs MuRAG overall F1 deficit (paper: 51.85%)",
"recomputed": 51.8519,
"paper_states": 51.85,
"tol": 0.01,
"ok": true
},
{
"claim": "A-Mem refusal F1 deficit vs FIFO (paper: 7.22%)",
"recomputed": 3.1625,
"paper_states": 7.22,
"tol": 0.01,
"ok": false
},
{
"claim": "MuRAG refusal F1 deficit vs FIFO (paper: 5.82%)",
"recomputed": 1.7013,
"paper_states": 5.82,
"tol": 0.01,
"ok": false
}
],
"n": 10,
"failed": 6,
"best_textual_model": "A-Mem",
"best_textual_overall_f1": 0.6228,
"source_row": "Table 3 Overall F1 (paper.raw.txt line 846)"
}
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""diag_denominators.py -- find which baseline each paper percentage actually uses.
The paper states percentages; the table holds F1. If the stated percentage does
not come out of the best-textual baseline, this brute-forces every model as the
denominator so the archive reports the real one instead of guessing.
"""
import itertools
import json
import os
import sys
BASE = os.path.dirname(os.path.abspath(__file__))
d = json.load(open(os.path.join(BASE, "claims.json"), encoding="utf-8"))
# Rebuild the numbers from the same source verify_claims.py uses.
sys.path.insert(0, BASE)
import verify_claims as vc # noqa: E402
def pct(a, b):
return (a - b) / b * 100.0
TARGETS = [
("FR", "MuRAG", 7.69), ("VS", "MuRAG", 12.29), ("TTL", "MuRAG", 29.06),
]
print("== which denominator reproduces each stated task gain? ==")
for task, num_model, target in TARGETS:
row = vc.TASK_F1[task]
num = row[num_model]
hits = []
for m, v in row.items():
got = pct(num, v)
if abs(got - target) <= 0.02:
hits.append((m, round(got, 3)))
print("%-4s stated=%6.2f%% numerator=%s(%.4f) matching denominators=%s" %
(task, target, num_model, num, hits or "NONE"))
# also show every candidate, so 'none' is visible rather than assumed
print(" all: " + ", ".join("%s=%+.2f%%" % (m, pct(num, v)) for m, v in row.items()))
print()
print("== AR: is 7.22 / 5.82 relative or absolute? ==")
fifo = vc.AR_F1["FIFO"]
for m in ("A-Mem", "MuRAG"):
rel = -pct(vc.AR_F1[m], fifo)
ab = (fifo - vc.AR_F1[m]) * 100.0
print(" %s vs FIFO: relative=%6.2f%% absolute_points=%6.2f F1=%.4f" %
(m, rel, ab, vc.AR_F1[m]))
print()
print("== Full(MM) 8.08: relative vs absolute? ==")
fm, ft = vc.OVERALL_F1["Full (MM)"], vc.OVERALL_F1["Full (Text)"]
print(" vs Full(Text): relative=%6.2f%% absolute_points=%6.2f" % (-pct(fm, ft), (ft - fm) * 100.0))
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""extract_figures.py -- pull the raster figures out of the frozen PDF and hash them.
Multimodal archive, so the images are part of the record, not an afterthought.
Figures are extracted as PNG (lossless) and each gets a sha256 in figures.json.
"""
import hashlib
import json
import os
import re
import subprocess
BASE = os.path.dirname(os.path.abspath(__file__))
PDF = os.path.join(BASE, "raw", "paper.pdf")
OUT = os.path.join(BASE, "figures")
def main():
os.makedirs(OUT, exist_ok=True)
if not os.path.exists(PDF):
raise SystemExit("missing raw/paper.pdf -- run fetch_source.py first")
prefix = os.path.join(BASE, "_figtmp")
r = subprocess.run(["pdfimages", "-png", PDF, prefix],
capture_output=True, text=True, timeout=600)
if r.returncode != 0:
raise SystemExit("pdfimages rc=%d: %s" % (r.returncode, r.stderr.strip()))
produced = sorted(f for f in os.listdir(BASE) if f.startswith("_figtmp"))
records = []
for name in produced:
src = os.path.join(BASE, name)
# pdfimages writes "<prefix>-NNN.ext"; strip the prefix, keep a sane name
base = name[len("_figtmp"):].lstrip("-")
dest = os.path.join(OUT, base)
os.replace(src, dest)
raw = open(dest, "rb").read()
records.append({"file": "figures/" + base, "bytes": len(raw),
"sha256": hashlib.sha256(raw).hexdigest()})
# which page each figure landed on, for citation
listing = subprocess.run(["pdfimages", "-list", PDF],
capture_output=True, text=True, timeout=300).stdout
pages = {}
for line in listing.split("\n")[2:]:
parts = line.split()
if len(parts) > 1 and parts[0].isdigit():
pages.setdefault(int(parts[0]), 0)
pages[int(parts[0])] += 1
payload = {
"source_pdf_sha256": hashlib.sha256(open(PDF, "rb").read()).hexdigest(),
"extractor": "pdfimages -png",
"n_images_in_pdf": len(produced),
"images_per_page": {str(k): v for k, v in sorted(pages.items())},
"figures": records,
}
with open(os.path.join(BASE, "figures.json"), "w", encoding="utf-8") as fh:
json.dump(payload, fh, indent=2)
fh.write("\n")
print("extracted %d images -> figures/" % len(records))
for r in records[:6]:
print(" %-28s %7d %s" % (os.path.basename(r["file"]), r["bytes"], r["sha256"][:16]))
if len(records) > 6:
print(" ... and %d more" % (len(records) - 6))
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""extract_text.py -- pdftotext the frozen PDF, hash the text, pull all authors."""
import hashlib
import html
import os
import re
import subprocess
BASE = os.path.dirname(os.path.abspath(__file__))
PDF = os.path.join(BASE, "raw", "paper.pdf")
TXT = os.path.join(BASE, "paper.txt")
def main():
if not os.path.exists(PDF):
raise SystemExit("missing raw/paper.pdf -- run fetch_source.py first")
r = subprocess.run(["pdftotext", "-layout", PDF, TXT],
capture_output=True, text=True, timeout=300)
if r.returncode != 0:
raise SystemExit("pdftotext rc=%d: %s" % (r.returncode, r.stderr.strip()))
raw = open(TXT, "rb").read()
print("paper.txt bytes=%d sha256=%s" % (len(raw), hashlib.sha256(raw).hexdigest()))
t = raw.decode("utf-8", errors="replace")
print("chars=%d lines=%d" % (len(t), t.count("\n")))
# authors from the arXiv abs page meta tags (the PDF byline is layout-scrambled)
a = open(os.path.join(BASE, "raw", "abs.html"), encoding="utf-8", errors="replace").read()
authors = re.findall(r'<meta name="citation_author" content="([^"]*)"', a)
print("authors(%d): %s" % (len(authors), "; ".join(html.unescape(x) for x in authors)))
with open(os.path.join(BASE, "text_manifest.json"), "w", encoding="utf-8") as fh:
import json
json.dump({"file": "paper.txt", "sha256": hashlib.sha256(raw).hexdigest(),
"chars": len(t), "lines": t.count("\n"),
"authors": [html.unescape(x) for x in authors]}, fh, indent=2)
fh.write("\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,27 @@
{
"arxiv_id": "2601.03515",
"fetched_utc": "2026-10-21T18:54:00Z",
"sources": [
{
"file": "raw/abs.html",
"url": "https://arxiv.org/abs/2601.03515",
"http_status": 200,
"bytes": 43359,
"sha256": "022aed61b37e9161a9f96d64be0f9e8fd9a3cc1131ef232c3bdb6f385b09f022"
},
{
"file": "raw/api.xml",
"url": "https://export.arxiv.org/api/query?id_list=2601.03515&max_results=1",
"http_status": 200,
"bytes": 3295,
"sha256": "7e885f063961c9345c77dceb094c37842748f7b05591da343bf63c1ad1efb41f"
},
{
"file": "raw/paper.pdf",
"url": "https://arxiv.org/pdf/2601.03515v1",
"http_status": 200,
"bytes": 3098382,
"sha256": "afb731ea1d9385e158565ef76f015762fd79ef300cbdb7659359ca77ff1884db"
}
]
}
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""fetch_source.py -- pull arXiv:2601.03515v1 into raw/ and freeze it with hashes.
Nothing here trusts the network for a claim: every byte on disk gets a sha256, and
manifest.json records what was fetched, from which URL, at what UTC time.
"""
import hashlib
import json
import os
import sys
import urllib.request
BASE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(BASE, "raw")
os.makedirs(RAW, exist_ok=True)
AID = "2601.03515"
URLS = {
"abs.html": "https://arxiv.org/abs/%s" % AID,
"api.xml": "https://export.arxiv.org/api/query?id_list=%s&max_results=1" % AID,
"paper.pdf": "https://arxiv.org/pdf/%sv1" % AID,
}
def fetch(name, url, timeout=120):
dest = os.path.join(RAW, name)
req = urllib.request.Request(url, headers={"User-Agent": "sphinx-archive/1.0 (research)"})
with urllib.request.urlopen(req, timeout=timeout) as r:
body = r.read()
code = r.getcode()
with open(dest, "wb") as fh:
fh.write(body)
sha = hashlib.sha256(body).hexdigest()
return {"file": "raw/" + name, "url": url, "http_status": code,
"bytes": len(body), "sha256": sha}
def main():
out = {"arxiv_id": AID, "fetched_utc": None, "sources": []}
import datetime
out["fetched_utc"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
for name, url in URLS.items():
try:
rec = fetch(name, url)
except Exception as exc: # noqa: BLE001 - recorded, not hidden
rec = {"file": "raw/" + name, "url": url, "error": repr(exc)}
out["sources"].append(rec)
print("FAIL %s: %r" % (name, exc), file=sys.stderr)
continue
out["sources"].append(rec)
print("%-10s http=%s bytes=%d sha256=%s" % (name, rec["http_status"], rec["bytes"], rec["sha256"][:16]))
with open(os.path.join(BASE, "fetch_manifest.json"), "w", encoding="utf-8") as fh:
json.dump(out, fh, indent=2, sort_keys=False)
fh.write("\n")
ok = [s for s in out["sources"] if "sha256" in s]
print("fetched %d/%d sources" % (len(ok), len(URLS)))
return 0 if len(ok) == len(URLS) else 1
if __name__ == "__main__":
sys.exit(main())
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Loaded 100 of 660 files, more files were not shown because too many files have changed in this diff. Show more