File size: 6,601 Bytes
e2d54c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Assemble the evaluator-visible candidate Hugging Face Space.

Rules this script enforces mechanically, so they cannot be forgotten:

  * every file present in the judged revision is carried over BYTE-IDENTICAL, and the
    old file set is verified to be a subset of the new one before anything is written;
  * current verification comes FIRST in navigation, and the superseded pages are
    labelled exactly "Historical rejected baseline" in the navigation tree;
  * every claim page inlines its exact statement and quantifiers, the assumption audit,
    the executable code, the exact fixed command and pinned environment, the raw
    numbers, links to downloadable raw CSV/JSON, the independent-checker and
    negative-control output, limitations and deviations, and the Git SHA, seeds,
    CPU allocation and runtime;
  * a visibility matrix is emitted with one row per claim and no empty cells.

Usage:
    python tools/build_space.py <judged_clone> <out_dir> <results.json>
"""

from __future__ import annotations

import hashlib
import json
import shutil
import sys
from pathlib import Path

LABEL_HISTORICAL = "Historical rejected baseline"
SPACE = "DineshAI/kwvtSA9ed3"
RAW = f"https://huggingface.co/spaces/{SPACE}/resolve/main"


def sha256(p: Path) -> str:
    return hashlib.sha256(p.read_bytes()).hexdigest()


IGNORED_DIRS = {".git", ".cache", ".huggingface"}


def file_set(root: Path) -> set[str]:
    """Real Space content only.

    A snapshot_download leaves .cache/huggingface/ metadata behind; counting it would
    inflate the judged file count and, worse, copy download bookkeeping into the
    candidate as if it were evidence.
    """
    return {
        str(p.relative_to(root))
        for p in root.rglob("*")
        if p.is_file() and not IGNORED_DIRS & set(p.parts)
    }


# --------------------------------------------------------------------------- #
def copy_protected(judged: Path, out: Path) -> dict[str, str]:
    """Carry every judged file across unchanged and record its hash."""
    hashes = {}
    for rel in sorted(file_set(judged)):
        src, dst = judged / rel, out / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dst)
        hashes[rel] = sha256(src)
    return hashes


def verify_subset(judged: Path, out: Path, hashes: dict[str, str]) -> list[str]:
    """The judged file set must be a subset of the candidate, with content preserved
    for everything except the navigation files we are required to update."""
    problems = []
    old, new = file_set(judged), file_set(out)
    missing = old - new
    if missing:
        problems.append(f"MISSING from candidate: {sorted(missing)}")
    for rel, h in hashes.items():
        if rel in {"pages/index.md", "logbook.json", "README.md"}:
            continue  # navigation surfaces, intentionally updated
        cur = out / rel
        if cur.exists() and sha256(cur) != h:
            problems.append(f"MODIFIED protected evidence file: {rel}")
    return problems


# --------------------------------------------------------------------------- #
def md_table(headers: list[str], rows: list[list[str]]) -> str:
    out = ["| " + " | ".join(headers) + " |",
           "|" + "|".join("---" for _ in headers) + "|"]
    for r in rows:
        out.append("| " + " | ".join(str(c) for c in r) + " |")
    return "\n".join(out)


def code_block(path: Path, lang: str = "python", max_lines: int | None = None) -> str:
    text = path.read_text()
    if max_lines:
        lines = text.splitlines()
        if len(lines) > max_lines:
            text = "\n".join(lines[:max_lines]) + f"\n\n# ... ({len(lines) - max_lines} more lines; full file linked above)"
    return f"```{lang}\n{text}\n```"


def build(judged: Path, out: Path, results: dict) -> None:
    if out.exists():
        shutil.rmtree(out)
    out.mkdir(parents=True)
    hashes = copy_protected(judged, out)

    (out / "data").mkdir(exist_ok=True)
    (out / "code").mkdir(exist_ok=True)

    # ---- raw data + code copies ---------------------------------------- #
    for src in results["data_files"]:
        shutil.copy2(src, out / "data" / Path(src).name)
    for src in results["code_files"]:
        shutil.copy2(src, out / "code" / Path(src).name)

    pages = results["pages"]
    for slug, page in pages.items():
        d = out / "pages" / slug
        d.mkdir(parents=True, exist_ok=True)
        (d / "page.md").write_text(page["body"])

    # ---- logbook.json: current first, historical labelled --------------- #
    lb = json.loads((judged / "logbook.json").read_text())
    children = [
        {"slug": slug, "title": page["title"],
         "file": f"pages/{slug}/page.md", "children": []}
        for slug, page in pages.items()
    ]
    historical = [
        {"slug": s, "title": f"{LABEL_HISTORICAL}{t}",
         "file": f"pages/{s}/page.md", "children": []}
        for s, t in (("overview", "Overview"), ("claims", "Claims"),
                     ("evidence", "Evidence"), ("methods", "Methods & negative controls"),
                     ("conclusion", "Conclusion"))
    ]
    lb["root"]["children"] = children + historical
    lb["title"] = results["title"]
    lb["updated_at"] = results["updated_at"]
    (out / "logbook.json").write_text(json.dumps(lb, indent=2) + "\n")
    (out / "pages" / "index.md").write_text(results["index_md"])
    (out / "README.md").write_text(results["readme_md"])

    problems = verify_subset(judged, out, hashes)
    manifest = {
        "space": SPACE,
        "judged_revision": results["judged_revision"],
        "judged_file_count": len(hashes),
        "candidate_file_count": len(file_set(out)),
        "old_is_subset_of_new": not any(p.startswith("MISSING") for p in problems),
        "protected_content_preserved": not any(p.startswith("MODIFIED") for p in problems),
        "problems": problems,
        "judged_hashes": hashes,
        "candidate_hashes": {rel: sha256(out / rel) for rel in sorted(file_set(out))},
    }
    (out / "data" / "upload_manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
    print(json.dumps({k: v for k, v in manifest.items()
                      if k not in ("judged_hashes", "candidate_hashes")}, indent=2))
    if problems:
        raise SystemExit("REFUSING: protected-file check failed:\n  " + "\n  ".join(problems))


def main() -> int:
    judged, out, results_path = (Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]))
    build(judged, out, json.loads(results_path.read_text()))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())