Spaces:
Paused
Paused
Sync GitHub main 6bb97fb9a95926f8118bd61bec366af28afa0e5c (part 7)
Browse files- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-0.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-1.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-42.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-0.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-1.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-42.txt +0 -0
- research/reviews/DETERMINISM_REPRODUCTION_20261024/reproduce.py +102 -0
- research/reviews/MANUSCRIPT_FOLLOWUP_20261024/README.md +30 -0
- research/reviews/MANUSCRIPT_FOLLOWUP_20261024/maturity-score-audit.json +73 -0
- research/reviews/PRIOR_ART_REVIEW_20261024.md +69 -0
- review_portal/build_static.py +59 -0
- review_portal/privacy-export.html +21 -0
- review_portal/tests/browser_static.py +71 -0
- review_portal/tests/test_static_export.py +32 -0
- scripts/check_scientific_maturity_integrity.py +19 -0
- scripts/check_stage1_frozen_prefreeze.py +48 -0
- scripts/freeze_stage1_topology_reference_r2.py +25 -6
- scripts/publication_empirical.py +1 -1
- scripts/run_emb001b_unseen_v4.py +238 -0
- scripts/run_stage1_reference_topology_mapping_r2.py +6 -1
- scripts/verify_determinism_reproduction.py +29 -0
- scripts/verify_emb001b_unseen_v4.py +106 -0
- scripts/verify_emb002a_delay_sweep_v4.py +12 -3
- scripts/verify_stage1_reference_dynamics_reuse_r2.py +3 -1
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-0.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-1.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stderr-42.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-0.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-1.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/artifacts/stdout-42.txt
ADDED
|
File without changes
|
research/reviews/DETERMINISM_REPRODUCTION_20261024/reproduce.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Source-bound reproduction; invoke with CLEAN_CHECKOUT OUTPUT_DIRECTORY."""
|
| 2 |
+
import dataclasses
|
| 3 |
+
import datetime
|
| 4 |
+
import gzip
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import platform
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def digest(data):
|
| 15 |
+
return hashlib.sha256(data).hexdigest()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def write(path, value):
|
| 19 |
+
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8", newline="\n")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
root = Path(sys.argv[1]).resolve()
|
| 23 |
+
output = Path(sys.argv[2]).resolve()
|
| 24 |
+
if len(sys.argv) > 3 and sys.argv[3] == "--worker":
|
| 25 |
+
sys.path.insert(0, str(root))
|
| 26 |
+
import yaml
|
| 27 |
+
from src.research.experiment_suite import run_ping_v2
|
| 28 |
+
config = yaml.safe_load((root / "configs/learning_experiment.yaml").read_text(encoding="utf-8"))
|
| 29 |
+
write(output, [dataclasses.asdict(run) for run in run_ping_v2(config, seeds=(101, 102, 103), ticks=256)])
|
| 30 |
+
sys.exit(0)
|
| 31 |
+
|
| 32 |
+
output.mkdir(parents=True, exist_ok=False)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def git(*args):
|
| 36 |
+
return subprocess.check_output(["git", "-c", f"safe.directory={root.as_posix()}", "-c", "core.autocrlf=false", *args], cwd=root).decode("utf-8").strip()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
assert not git("status", "--porcelain", "--untracked-files=all"), "Checkout must be clean"
|
| 40 |
+
files = {}
|
| 41 |
+
for line in git("ls-files", "-s", "src", "configs").splitlines():
|
| 42 |
+
metadata, name = line.split("\t", 1)
|
| 43 |
+
blob = metadata.split()[1]
|
| 44 |
+
data = (root / name).read_bytes()
|
| 45 |
+
actual_blob = hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()
|
| 46 |
+
assert actual_blob == blob, f"Non-exact source bytes: {name}"
|
| 47 |
+
files[name] = {"git_blob": blob, "sha256": digest(data)}
|
| 48 |
+
historical = root / "research/experiments/EXP-BATCH-20260914074039-03"
|
| 49 |
+
config_hash = files["configs/learning_experiment.yaml"]["sha256"]
|
| 50 |
+
old_manifest = json.loads((historical / "manifest.json").read_text())
|
| 51 |
+
assert config_hash == old_manifest["config"]["sha256"], "Historical config differs"
|
| 52 |
+
freeze = {
|
| 53 |
+
"created_before_execution_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
| 54 |
+
"commit": git("rev-parse", "HEAD"), "tree": git("rev-parse", "HEAD^{tree}"),
|
| 55 |
+
"status_before": git("status", "--porcelain", "--untracked-files=all"),
|
| 56 |
+
"source_and_config": files, "config_matches_historical": True,
|
| 57 |
+
"protocol": "deterministic_replica_v1", "runner": "src.research.experiment_suite.run_ping_v2",
|
| 58 |
+
"seeds": [101, 102, 103], "ticks": 256,
|
| 59 |
+
"python": sys.version, "executable": sys.executable, "platform": platform.platform(),
|
| 60 |
+
"machine": platform.machine(), "processor": platform.processor(),
|
| 61 |
+
"harness_sha256": digest(Path(__file__).read_bytes()),
|
| 62 |
+
"command": [sys.executable, *sys.argv],
|
| 63 |
+
"comparison": "All metrics plus before/after state digests in A/B pairs; historical four manuscript totals; full output across fresh processes",
|
| 64 |
+
"scientific_evidence": False,
|
| 65 |
+
}
|
| 66 |
+
write(output / "freeze.json", freeze)
|
| 67 |
+
(output / "pip-freeze.txt").write_bytes(subprocess.check_output([sys.executable, "-m", "pip", "freeze", "--all"]))
|
| 68 |
+
processes = []
|
| 69 |
+
for hashseed in (0, 1, 42):
|
| 70 |
+
target = output / f"runs-hashseed-{hashseed}.json"
|
| 71 |
+
command = [sys.executable, "-B", str(Path(__file__).resolve()), str(root), str(target), "--worker"]
|
| 72 |
+
env = dict(os.environ, PYTHONHASHSEED=str(hashseed), PYTHONDONTWRITEBYTECODE="1")
|
| 73 |
+
result = subprocess.run(command, cwd=root, env=env, capture_output=True)
|
| 74 |
+
(output / f"stdout-{hashseed}.txt").write_bytes(result.stdout)
|
| 75 |
+
(output / f"stderr-{hashseed}.txt").write_bytes(result.stderr)
|
| 76 |
+
processes.append({"command": command, "PYTHONHASHSEED": hashseed, "exit_code": result.returncode})
|
| 77 |
+
write(output / "processes.json", processes)
|
| 78 |
+
if result.returncode:
|
| 79 |
+
raise RuntimeError(result.stderr.decode(errors="replace"))
|
| 80 |
+
|
| 81 |
+
runs = json.loads((output / "runs-hashseed-0.json").read_text())
|
| 82 |
+
checks = []
|
| 83 |
+
keys = ("total_spikes", "delivered_synaptic_events", "recurrent_events", "propagation_depth")
|
| 84 |
+
for seed in (101, 102, 103):
|
| 85 |
+
for arm, expected in (("recurrence_off", [3, 2, 0, 1]), ("recurrence_on", [33, 33, 10, 61])):
|
| 86 |
+
a, b = [next(r for r in runs if r["seed"] == seed and r["condition"] == f"{arm}_replica_{replica}") for replica in ("a", "b")]
|
| 87 |
+
old = json.loads(gzip.decompress(next((historical / "DATA/raw").glob(f"*{arm}_replica_a-seed-{seed}.json.gz")).read_bytes()))
|
| 88 |
+
checks.append({"seed": seed, "arm": arm,
|
| 89 |
+
"all_metrics_equal": a["metrics"] == b["metrics"],
|
| 90 |
+
"initial_state_equal": a["state_digest_before"] == b["state_digest_before"],
|
| 91 |
+
"final_state_equal": a["state_digest_after"] == b["state_digest_after"],
|
| 92 |
+
"ticks_satisfied": a["metrics"]["ticks_executed"] == b["metrics"]["ticks_executed"] == 256,
|
| 93 |
+
"no_runtime_errors": a["runtime_error"] is None and b["runtime_error"] is None,
|
| 94 |
+
"observed": [a["metrics"][key] for key in keys], "expected": expected,
|
| 95 |
+
"historical_totals": [old["metrics"][key] for key in keys]})
|
| 96 |
+
equal_processes = len({digest((output / f"runs-hashseed-{seed}.json").read_bytes()) for seed in (0, 1, 42)}) == 1
|
| 97 |
+
status_after = git("status", "--porcelain", "--untracked-files=all")
|
| 98 |
+
passed = equal_processes and not status_after and all(all(c[k] for k in ("all_metrics_equal", "initial_state_equal", "final_state_equal", "ticks_satisfied", "no_runtime_errors")) and c["observed"] == c["expected"] == c["historical_totals"] for c in checks)
|
| 99 |
+
write(output / "result.json", {"passed": passed, "checks": checks, "fresh_process_outputs_identical": equal_processes, "status_after": status_after, "metric_order": keys, "run_count": 36, "pair_count": 18})
|
| 100 |
+
write(output / "sha256sums.json", {p.name: digest(p.read_bytes()) for p in sorted(output.iterdir()) if p.is_file()})
|
| 101 |
+
print(json.dumps({"passed": passed, "checks": checks, "fresh_process_outputs_identical": equal_processes}, indent=2))
|
| 102 |
+
sys.exit(0 if passed else 1)
|
research/reviews/MANUSCRIPT_FOLLOWUP_20261024/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Manuscript-review follow-up — 2026-10-24
|
| 2 |
+
|
| 3 |
+
This work follows the five remaining items of the 2026-10-17 TODO. It is
|
| 4 |
+
recorded on a local branch; publication and independent review are not implied.
|
| 5 |
+
|
| 6 |
+
| Item | Delivered | Remaining boundary |
|
| 7 |
+
| --- | --- | --- |
|
| 8 |
+
| 10% memory compression | [R1 protocol](../../preregistrations/PREREG-MEM-COMPRESSION-R1.md): complete accounting categories, matched-byte controls, 95% retention/non-inferiority rule, stop and architecture decision rules | REVIEW_READY_NOT_FROZEN: canonical RQ/H admission, byte-accounting/restore implementation tests, dataset/source freeze and R1 human approval remain. No experiment executed. |
|
| 9 |
+
| H-EMB-001-B | [V4 report](../../experiments/EXP-EMB001B-UNSEEN-V4-20261024/report.md): 120 runs, disturbance unknown to donor tape, matched causal controls, all integrity checks passed | Exploratory synthetic DATA only; human scientific review and EVID promotion pending. |
|
| 10 |
+
| 5D ablation | [R2 design](../../preregistrations/PREREG-5D-MATCHED-R2.md) and [hash freeze](../../preregistrations/PREREG-5D-MATCHED-R2.freeze.json) | Design frozen before evaluation; dedicated executable R2 runner, preflight and authorization still required. Does not bypass blocked efficiency R1. |
|
| 11 |
+
| Prior art | [Source-based assessment](../PRIOR_ART_REVIEW_20261024.md), explicit antecedents and bounded novelty wording | Independent external reviewer has not reviewed it; the independence TODO remains open. |
|
| 12 |
+
| Maturity percentages | [Method](../../../docs/05-quality/SCIENTIFIC_MATURITY_METHOD.md), arithmetic audit, corrected dashboard and enforcing checker | Status labels remain judgment-based; no evidence promotion and no probability interpretation. Historical editions unchanged. |
|
| 13 |
+
|
| 14 |
+
The memorandum is an addendum to edition 1.8, not a silent rewriting of its
|
| 15 |
+
published historical results. New DATA should enter a future publication only
|
| 16 |
+
with the scoped limitations and review status above. Source-bound experiment
|
| 17 |
+
details live in its own freeze record, not in the later documentation commit.
|
| 18 |
+
|
| 19 |
+
The memory R1 plan is concrete enough for methodological review but cannot be
|
| 20 |
+
called a full freeze merely because its prose is finished. The implementation
|
| 21 |
+
work listed there is required before an execution request. No result-dependent
|
| 22 |
+
choice, favorable outcome or automatic score change substitutes for review.
|
| 23 |
+
|
| 24 |
+
Validation: 43 focused tests passed (embodiment compatibility/controls,
|
| 25 |
+
publication maturity contracts and deliberate score-corruption cases).
|
| 26 |
+
Ruff, JavaScript syntax, documentation consistency and scientific-maturity
|
| 27 |
+
integrity checks passed. `verify_emb001b_unseen_v4.py` independently recomputes
|
| 28 |
+
the primary outcome from all 120 stored traces and checks archived hashes,
|
| 29 |
+
causal sentinels and source hashes against the pre-execution Git commit.
|
| 30 |
+
This second computation is an internal verifier, not independent peer review.
|
research/reviews/MANUSCRIPT_FOLLOWUP_20261024/maturity-score-audit.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_commit": "fdfd0568",
|
| 3 |
+
"criteria_changed": false,
|
| 4 |
+
"evidence_promoted": false,
|
| 5 |
+
"stages": [
|
| 6 |
+
{
|
| 7 |
+
"stage": 0,
|
| 8 |
+
"previous_score": 0.925,
|
| 9 |
+
"weighted_status_score": 0.925,
|
| 10 |
+
"difference": 0.0
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"stage": 1,
|
| 14 |
+
"previous_score": 0.85,
|
| 15 |
+
"weighted_status_score": 0.85,
|
| 16 |
+
"difference": 0.0
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stage": 2,
|
| 20 |
+
"previous_score": 0.65,
|
| 21 |
+
"weighted_status_score": 0.75,
|
| 22 |
+
"difference": 0.1
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"stage": 3,
|
| 26 |
+
"previous_score": 0.55,
|
| 27 |
+
"weighted_status_score": 0.65,
|
| 28 |
+
"difference": 0.1
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"stage": 4,
|
| 32 |
+
"previous_score": 0.55,
|
| 33 |
+
"weighted_status_score": 0.65,
|
| 34 |
+
"difference": 0.1
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"stage": 5,
|
| 38 |
+
"previous_score": 0.55,
|
| 39 |
+
"weighted_status_score": 0.65,
|
| 40 |
+
"difference": 0.1
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"stage": 6,
|
| 44 |
+
"previous_score": 0.4,
|
| 45 |
+
"weighted_status_score": 0.45,
|
| 46 |
+
"difference": 0.05
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"stage": 7,
|
| 50 |
+
"previous_score": 0.28,
|
| 51 |
+
"weighted_status_score": 0.325,
|
| 52 |
+
"difference": 0.045
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"stage": 8,
|
| 56 |
+
"previous_score": 0.23,
|
| 57 |
+
"weighted_status_score": 0.4,
|
| 58 |
+
"difference": 0.17
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"stage": 9,
|
| 62 |
+
"previous_score": 0.15,
|
| 63 |
+
"weighted_status_score": 0.3,
|
| 64 |
+
"difference": 0.15
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"stage": 10,
|
| 68 |
+
"previous_score": 0.13,
|
| 69 |
+
"weighted_status_score": 0.3,
|
| 70 |
+
"difference": 0.17
|
| 71 |
+
}
|
| 72 |
+
]
|
| 73 |
+
}
|
research/reviews/PRIOR_ART_REVIEW_20261024.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Prior-art assessment of the methodology claims
|
| 2 |
+
|
| 3 |
+
2026-10-24. **AI-assisted source review; independent external review PENDING.**
|
| 4 |
+
The assistant preparing this review also changed repository methods and
|
| 5 |
+
documentation. It is not an independent reviewer. No external person has
|
| 6 |
+
been contacted or represented as having approved this assessment.
|
| 7 |
+
|
| 8 |
+
## Search and inclusion record
|
| 9 |
+
|
| 10 |
+
Web search on 2026-10-24, queries: “research provenance W3C PROV
|
| 11 |
+
recommendation 2013 executable paper computational reproducibility Knuth
|
| 12 |
+
1984 literate programming”; “Nosek 2018 preregistration revolution reflexivity
|
| 13 |
+
scientific research positionality Finlay 2002”; “eLife executable research
|
| 14 |
+
2020”; “Jupyter Notebooks 2016 Kluyver”; “site.cos.io registered reports peer
|
| 15 |
+
review before results”. Included original standards, author publications,
|
| 16 |
+
institutional author deposits and publisher descriptions directly addressing
|
| 17 |
+
one of the four claims. General blog summaries and search aggregators were
|
| 18 |
+
excluded. This is a targeted scoping review, not an exhaustive systematic
|
| 19 |
+
search or a patent novelty opinion. Ranking, language and database coverage
|
| 20 |
+
can omit relevant work; absence from this table is not absence of prior art.
|
| 21 |
+
|
| 22 |
+
## Claim-to-source comparison
|
| 23 |
+
|
| 24 |
+
| MHRN claim family | Prior art and verified scope | Consequence for novelty |
|
| 25 |
+
| --- | --- | --- |
|
| 26 |
+
| Research provenance | [W3C PROV-DM (2013)](https://www.w3.org/TR/prov-dm/) models entities, activities, agents and derivation. | Recording origins and transformations is established. MHRN's hashes, DATA/EVID distinctions and domain-specific gates are implementation choices; they do not invent provenance. No formal PROV interoperability has been demonstrated here. |
|
| 27 |
+
| Preregistration | [Nosek et al. (2018)](https://pmc.ncbi.nlm.nih.gov/articles/5856500/) distinguish prospective predictions/analysis plans from postdiction. [COS Registered Reports](https://www.cos.io/initiatives/registered-reports) describes review before data collection. | Frozen plans and separating exploratory from confirmatory work are established. A local Git freeze is not independent registry deposit or a journal's in-principle acceptance. |
|
| 28 |
+
| Executable publication | [Knuth's author account of literate programming](https://cs.stanford.edu/~knuth/lp.html) combines program and documentation; [Kluyver et al. (2016)](https://escholarship.org/uc/item/08b3d4s2) presents notebooks as reproducible-workflow publication; [eLife's ERA launch (2020)](https://elifesciences.org/for-the-press/eb096af1/elife-launches-executable-research-articles-for-publishing-computationally-reproducible-results) embeds executable code and outputs. | Linking narrative, code and results is not new. MHRN's source-bound publication pipeline is a project-specific realization; an exportable manuscript or artifact link alone is not an in-browser executable article. |
|
| 29 |
+
| Reflexive methodology | [Finlay (2002)](https://doi.org/10.1177/146879410200200205) discusses several forms of researcher reflexivity and their difficulties. Only the publisher's abstract and accessible page were inspected. | Documenting researcher influence, choices and limitations has substantial antecedents. AI participation adds a context needing evaluation, not an automatic claim of a new epistemology. |
|
| 30 |
+
| Independent verification | [COS TOP Guidelines](https://www.cos.io/initiatives/top-guidelines) distinguishes verification by an independent party, reproducibility and replication. | Passing local checks or reusing a different software implementation does not by itself establish independent replication. |
|
| 31 |
+
|
| 32 |
+
All descriptions above are paraphrases. The Knuth original journal DOI
|
| 33 |
+
`10.1093/comjnl/27.2.97` was identified, but its full publisher page could not
|
| 34 |
+
be retrieved; the author's accessible overview is used for the limited claim.
|
| 35 |
+
No inaccessible full text is presented as read.
|
| 36 |
+
|
| 37 |
+
## Assessment of the actual manuscript claim
|
| 38 |
+
|
| 39 |
+
Edition 1.8, `parts/10_synthesis.md`, locates the project's contribution in
|
| 40 |
+
integration of research software engineering and governance: RQ/H registry,
|
| 41 |
+
preregistration, source-bound DATA, human review, EVID status, provenance,
|
| 42 |
+
test adequacy and viewer. That modest integration claim is more defensible
|
| 43 |
+
than claiming invention of any component. It remains a description of work
|
| 44 |
+
performed, not proof that its combination is unprecedented or improves
|
| 45 |
+
scientific quality compared with existing workflows.
|
| 46 |
+
|
| 47 |
+
Recommended wording for subsequent editions:
|
| 48 |
+
|
| 49 |
+
> MHRN implements a domain-specific integration of established provenance,
|
| 50 |
+
> preregistration, reproducible-computation and reflexive-documentation
|
| 51 |
+
> practices. Its contribution is the explicit implementation and audit of
|
| 52 |
+
> these practices in this evolving SNN programme. Priority of the combination
|
| 53 |
+
> and comparative gains in research quality have not been established.
|
| 54 |
+
|
| 55 |
+
This recommendation does not silently rewrite an already published edition.
|
| 56 |
+
To establish a stronger claim, compare MHRN against established workflow and
|
| 57 |
+
publication systems on predefined capabilities and failure cases. Include
|
| 58 |
+
maintenance cost, usability, audit error rates and negative outcomes; a
|
| 59 |
+
feature inventory alone is not a causal evaluation of governance quality.
|
| 60 |
+
|
| 61 |
+
## External review handoff
|
| 62 |
+
|
| 63 |
+
An independent reviewer should disclose involvement/conflicts, expand the
|
| 64 |
+
search beyond these starting sources, inspect actual implementation and
|
| 65 |
+
manuscript passages, distinguish conceptual antecedents from engineering
|
| 66 |
+
differences, and return a dated signed assessment with cited counterexamples.
|
| 67 |
+
Acceptance requires a review artifact, reviewer identity/role, search scope,
|
| 68 |
+
claim-by-claim decisions and explicit unresolved limitations. Until then the
|
| 69 |
+
TODO's independence requirement remains open; this preparation is complete.
|
review_portal/build_static.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Package only public questionnaire assets, with online collection disabled."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
SOURCE = ROOT / "src/dashboard/static/review"
|
| 12 |
+
FILES = ("index.html", "app.js", "model.js", "instrument.js", "review.css")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def build(output: Path) -> dict[str, object]:
|
| 16 |
+
"""Create a new directory; never publish admin pages, secrets or response data."""
|
| 17 |
+
contents = {name: (SOURCE / name).read_bytes() for name in FILES}
|
| 18 |
+
html = contents["index.html"].decode("utf-8")
|
| 19 |
+
html = html.replace('<html lang="de">', '<html lang="de" data-review-mode="export-only">')
|
| 20 |
+
html = html.replace('="/review/', '="./')
|
| 21 |
+
html = html.replace("connect-src 'self'", "connect-src 'none'")
|
| 22 |
+
html = html.replace(
|
| 23 |
+
'<a href="./admin.html">Erhebungsverwaltung</a>',
|
| 24 |
+
'<a href="./privacy.html">Datenschutz und Exportmodus</a>',
|
| 25 |
+
)
|
| 26 |
+
if 'data-review-mode="export-only"' not in html or "admin.html" in html:
|
| 27 |
+
raise ValueError("Static export contract changed; inspect the source HTML")
|
| 28 |
+
contents["index.html"] = html.encode("utf-8")
|
| 29 |
+
contents["review.css"] += (
|
| 30 |
+
b'\nhtml[data-review-mode="export-only"] .withdrawal { display: none; }\n'
|
| 31 |
+
)
|
| 32 |
+
contents["privacy.html"] = (ROOT / "review_portal/privacy-export.html").read_bytes()
|
| 33 |
+
contents[".nojekyll"] = b""
|
| 34 |
+
manifest = {
|
| 35 |
+
"mode": "export-only",
|
| 36 |
+
"collection_enabled": False,
|
| 37 |
+
"files_sha256": {
|
| 38 |
+
name: hashlib.sha256(data).hexdigest() for name, data in contents.items()
|
| 39 |
+
},
|
| 40 |
+
"independent_review": "not_asserted",
|
| 41 |
+
}
|
| 42 |
+
output.mkdir(parents=True, exist_ok=False)
|
| 43 |
+
for name, data in contents.items():
|
| 44 |
+
(output / name).write_bytes(data)
|
| 45 |
+
(output / "deployment-manifest.json").write_text(
|
| 46 |
+
json.dumps(manifest, indent=2) + "\n", encoding="utf-8", newline="\n"
|
| 47 |
+
)
|
| 48 |
+
return manifest
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def main() -> None:
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 53 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 54 |
+
args = parser.parse_args()
|
| 55 |
+
print(json.dumps(build(args.output), indent=2))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
review_portal/privacy-export.html
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
| 3 |
+
<meta name="referrer" content="no-referrer">
|
| 4 |
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; base-uri 'none'; form-action 'none'">
|
| 5 |
+
<title>MHRN — Datenschutz und Exportmodus</title><link rel="stylesheet" href="./review.css"></head>
|
| 6 |
+
<body><header class="masthead"><a class="brand" href="./index.html">MHRN — zum Fragebogen</a></header>
|
| 7 |
+
<main><section class="card"><h1>Datenschutz und lokaler Export</h1>
|
| 8 |
+
<p>Stand: 24. Oktober 2026. Dieses Paket ist ausschließlich für den Exportmodus auf GitHub Pages vorbereitet.</p>
|
| 9 |
+
<h2>Verantwortlich für das Portal</h2>
|
| 10 |
+
<p>Thomas Heisig, Wolffsheide 10, 27777 Ganderkesee, Deutschland. Kontakt: <a href="mailto:t_heisig@gmx.de">t_heisig@gmx.de</a>.</p>
|
| 11 |
+
<h2>Ihre Antworten</h2>
|
| 12 |
+
<p>Es gibt hier keine zentrale Online-Abgabe. Antworten werden im Browser verarbeitet und nicht automatisch an die Projektleitung, GitHub oder einen KI-Dienst versendet. Das Ausfüllen ist freiwillig; persönliche und sensible Angaben sollten vermieden werden. Eine Bewertung ist keine unabhängige Ethikfreigabe oder wissenschaftliche Evidenz.</p>
|
| 13 |
+
<p>Ohne aktivierte Gerätespeicherung bleibt der Entwurf nur im Arbeitsspeicher der Seite. Auf Wunsch können Sie einen unverschlüsselten Entwurf im lokalen Browserspeicher ablegen und mit „Lokalen Entwurf löschen“ entfernen. Von Ihnen heruntergeladene JSON-/CSV-Dateien verwalten und löschen Sie selbst. Browserdaten können verloren gehen; das Portal erstellt keine Antwort-Backups.</p>
|
| 14 |
+
<p>Ein Export ist keine Serverabgabe. Für eine spätere Übermittlung an eine prüfende Person müssen Empfänger, Zweck, sicherer Übermittlungsweg und Aufbewahrung gesondert vereinbart werden. Dieses Portal verlangt keine Zusendung per E-Mail.</p>
|
| 15 |
+
<h2>Hosting und technische Verbindungsdaten</h2>
|
| 16 |
+
<p>Beim Abruf über GitHub Pages erhält der Hostingdienst technische Verbindungsdaten. GitHub erklärt, IP-Adressen von Seitenbesuchern zu Sicherheitszwecken zu protokollieren. Die Aussage „kein Antwortversand“ bedeutet deshalb nicht „keine Verbindungsdaten“.</p>
|
| 17 |
+
<p>Weitere Angaben zu GitHubs Verarbeitung, Speicherdauern, Übermittlungen und Datenschutzrechten stehen in der <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement" rel="noreferrer">GitHub-Datenschutzerklärung</a> und der <a href="https://docs.github.com/en/pages/getting-started-with-github-pages/what-is-github-pages#data-collection" rel="noreferrer">Pages-Dokumentation</a>. Das Portal verwendet keine eigenen Analyse-Tracker.</p>
|
| 18 |
+
<h2>Fragen und Rechte</h2>
|
| 19 |
+
<p>Für Fragen zur Verarbeitung durch die Projektleitung nutzen Sie den obigen Kontakt. Je nach Verarbeitung bestehen Rechte auf Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit und Widerspruch sowie ein Beschwerderecht bei einer Datenschutzaufsichtsbehörde. Eine etwaige Einwilligung kann für die Zukunft widerrufen werden. Lokale Antworten können wir nicht abrufen oder für Sie löschen.</p>
|
| 20 |
+
<p>Der Exportmodus aktiviert keine zentral gespeicherte Studie. Vor einer Online-Erhebung sind deren konkrete Rechtsgrundlage, Hosting, Zugriff, Fristen und unabhängige Prüfung gesondert festzulegen.</p>
|
| 21 |
+
</section></main></body></html>
|
review_portal/tests/browser_static.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exercise the export package under a project subpath with no collector."""
|
| 2 |
+
|
| 3 |
+
from functools import partial
|
| 4 |
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from tempfile import TemporaryDirectory
|
| 8 |
+
from threading import Thread
|
| 9 |
+
|
| 10 |
+
from playwright.sync_api import expect, sync_playwright
|
| 11 |
+
|
| 12 |
+
from review_portal.build_static import build
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class QuietHandler(SimpleHTTPRequestHandler):
|
| 16 |
+
def log_message(self, *args):
|
| 17 |
+
pass
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
with TemporaryDirectory(prefix="review-static-") as directory:
|
| 22 |
+
root = Path(directory)
|
| 23 |
+
build(root / "MHRN")
|
| 24 |
+
server = ThreadingHTTPServer(
|
| 25 |
+
("127.0.0.1", 0), partial(QuietHandler, directory=directory)
|
| 26 |
+
)
|
| 27 |
+
thread = Thread(target=server.serve_forever, daemon=True)
|
| 28 |
+
thread.start()
|
| 29 |
+
try:
|
| 30 |
+
with sync_playwright() as playwright:
|
| 31 |
+
browser = playwright.chromium.launch(
|
| 32 |
+
channel=os.environ.get("MHRN_TEST_BROWSER_CHANNEL")
|
| 33 |
+
)
|
| 34 |
+
page = browser.new_page(accept_downloads=True)
|
| 35 |
+
errors, requests = [], []
|
| 36 |
+
page.on("pageerror", lambda error: errors.append(str(error)))
|
| 37 |
+
page.on("request", lambda request: requests.append(request))
|
| 38 |
+
url = f"http://127.0.0.1:{server.server_port}/MHRN/index.html"
|
| 39 |
+
page.goto(url, wait_until="networkidle")
|
| 40 |
+
expect(page.locator("#mode")).to_contain_text("kein Versand")
|
| 41 |
+
expect(page.locator("a[href='./privacy.html']")).to_be_visible()
|
| 42 |
+
expect(page.locator(".withdrawal")).not_to_be_visible()
|
| 43 |
+
page.locator("#adult").check()
|
| 44 |
+
page.locator("#consent").check()
|
| 45 |
+
page.locator("#start").click()
|
| 46 |
+
page.locator('.question:has(.qid:text-is("A4")) textarea').first.fill(
|
| 47 |
+
"SYNTHETIC EXPORT TEST"
|
| 48 |
+
)
|
| 49 |
+
assert page.evaluate("localStorage.length") == 0
|
| 50 |
+
with page.expect_download() as download:
|
| 51 |
+
page.locator("#save").click()
|
| 52 |
+
assert "SYNTHETIC EXPORT TEST" in Path(download.value.path()).read_text(
|
| 53 |
+
encoding="utf-8"
|
| 54 |
+
)
|
| 55 |
+
page.set_viewport_size({"width": 390, "height": 844})
|
| 56 |
+
assert page.evaluate("document.documentElement.scrollWidth <= innerWidth")
|
| 57 |
+
expect(page.locator("#online")).not_to_be_visible()
|
| 58 |
+
assert not errors, errors
|
| 59 |
+
assert all("/api/" not in r.url and r.method == "GET" for r in requests)
|
| 60 |
+
page.goto(url.replace("index.html", "privacy.html"))
|
| 61 |
+
expect(page.locator("h1")).to_have_text("Datenschutz und lokaler Export")
|
| 62 |
+
browser.close()
|
| 63 |
+
finally:
|
| 64 |
+
server.shutdown()
|
| 65 |
+
server.server_close()
|
| 66 |
+
thread.join(timeout=5)
|
| 67 |
+
print("PASS: project subpath, offline export, privacy page, mobile; zero API requests")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
main()
|
review_portal/tests/test_static_export.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Static deployment must remain portable and unable to collect responses."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
from review_portal.build_static import FILES, build
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_only_export_assets_are_packaged(tmp_path):
|
| 12 |
+
output = tmp_path / "MHRN"
|
| 13 |
+
result = build(output)
|
| 14 |
+
assert set(p.name for p in output.iterdir()) == set(FILES) | {
|
| 15 |
+
"privacy.html", ".nojekyll", "deployment-manifest.json"
|
| 16 |
+
}
|
| 17 |
+
html = (output / "index.html").read_text(encoding="utf-8")
|
| 18 |
+
assert 'data-review-mode="export-only"' in html
|
| 19 |
+
assert "connect-src 'none'" in html
|
| 20 |
+
assert '="/review/' not in html
|
| 21 |
+
assert "admin.html" not in html
|
| 22 |
+
assert result["collection_enabled"] is False
|
| 23 |
+
stored = json.loads((output / "deployment-manifest.json").read_text())
|
| 24 |
+
for name, digest in stored["files_sha256"].items():
|
| 25 |
+
assert hashlib.sha256((output / name).read_bytes()).hexdigest() == digest
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_existing_directory_is_never_published_or_overwritten(tmp_path):
|
| 29 |
+
(tmp_path / "private-response.json").write_text("private")
|
| 30 |
+
with pytest.raises(FileExistsError):
|
| 31 |
+
build(tmp_path)
|
| 32 |
+
assert list(tmp_path.iterdir()) == [tmp_path / "private-response.json"]
|
scripts/check_scientific_maturity_integrity.py
CHANGED
|
@@ -90,6 +90,25 @@ def main() -> int:
|
|
| 90 |
isinstance(criteria, list) and bool(criteria),
|
| 91 |
f"stage {stage_no} missing scientific criteria",
|
| 92 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
stage6 = next(
|
| 95 |
(
|
|
|
|
| 90 |
isinstance(criteria, list) and bool(criteria),
|
| 91 |
f"stage {stage_no} missing scientific criteria",
|
| 92 |
)
|
| 93 |
+
if isinstance(criteria, list) and isinstance(weights, dict):
|
| 94 |
+
ids = [c.get("id") for c in criteria if isinstance(c, dict)]
|
| 95 |
+
require(
|
| 96 |
+
len(ids) == len(set(ids)) and set(ids) == set(weights),
|
| 97 |
+
f"stage {stage_no} must contain each weighted criterion once",
|
| 98 |
+
)
|
| 99 |
+
scale = {"met": 1.0, "partial": 0.5, "open": 0.0}
|
| 100 |
+
valid = all(
|
| 101 |
+
isinstance(c, dict) and c.get("status") in scale for c in criteria
|
| 102 |
+
)
|
| 103 |
+
require(valid, f"stage {stage_no} has an unsupported criterion status")
|
| 104 |
+
if valid and set(ids) == set(weights):
|
| 105 |
+
expected = sum(
|
| 106 |
+
float(weights[c["id"]]) * scale[c["status"]] for c in criteria
|
| 107 |
+
)
|
| 108 |
+
require(
|
| 109 |
+
abs(score - expected) < 1e-9,
|
| 110 |
+
f"stage {stage_no} score does not match weighted criteria",
|
| 111 |
+
)
|
| 112 |
|
| 113 |
stage6 = next(
|
| 114 |
(
|
scripts/check_stage1_frozen_prefreeze.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Revalidate frozen R2 methods without replacing hash-bound audit artifacts."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import importlib
|
| 7 |
+
import json
|
| 8 |
+
import sys
|
| 9 |
+
import tempfile
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
sys.path.insert(0, str(ROOT))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main() -> None:
|
| 17 |
+
freeze = json.loads(
|
| 18 |
+
(
|
| 19 |
+
ROOT / "research/preregistrations/PREREG-S1-TOPO-REFERENCE-R2.freeze.json"
|
| 20 |
+
).read_text(encoding="utf-8")
|
| 21 |
+
)
|
| 22 |
+
for key in (
|
| 23 |
+
"mechanism_audit",
|
| 24 |
+
"dynamics_reuse_validation",
|
| 25 |
+
"topology_mapping_parity",
|
| 26 |
+
"integrator_parity",
|
| 27 |
+
"reset_parity",
|
| 28 |
+
"synapse_delay_parity",
|
| 29 |
+
):
|
| 30 |
+
entry = freeze[key]
|
| 31 |
+
path = ROOT / entry["path"].replace("\\", "/")
|
| 32 |
+
if hashlib.sha256(path.read_bytes()).hexdigest() != entry["sha256"]:
|
| 33 |
+
raise ValueError(f"Frozen artifact changed: {key}")
|
| 34 |
+
with tempfile.TemporaryDirectory(prefix="mhrn-r2-revalidation-") as directory:
|
| 35 |
+
for name in (
|
| 36 |
+
"audit_stage1_reference_mechanisms",
|
| 37 |
+
"verify_stage1_reference_dynamics_reuse_r2",
|
| 38 |
+
"run_stage1_reference_topology_mapping_r2",
|
| 39 |
+
):
|
| 40 |
+
module = importlib.import_module(f"scripts.{name}")
|
| 41 |
+
module.OUT = Path(directory) / f"{name}.json"
|
| 42 |
+
if module.main() != 0:
|
| 43 |
+
raise ValueError(f"Current method revalidation failed: {name}")
|
| 44 |
+
print("PASS: frozen artifacts intact and current R2 method gates passed")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
main()
|
scripts/freeze_stage1_topology_reference_r2.py
CHANGED
|
@@ -3,9 +3,12 @@
|
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
|
|
|
| 6 |
import hashlib
|
| 7 |
import json
|
| 8 |
import subprocess
|
|
|
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -75,6 +78,14 @@ def git(*args):
|
|
| 75 |
|
| 76 |
|
| 77 |
def main() -> int:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if F.exists():
|
| 79 |
raise FileExistsError(F)
|
| 80 |
if git("status", "--porcelain"):
|
|
@@ -106,11 +117,11 @@ def main() -> int:
|
|
| 106 |
or proto.get("framework_version") != "2.10.1"
|
| 107 |
):
|
| 108 |
raise RuntimeError("R2 protocol mismatch")
|
| 109 |
-
subprocess.run([
|
| 110 |
-
subprocess.run([
|
| 111 |
subprocess.run(
|
| 112 |
[
|
| 113 |
-
|
| 114 |
"-m",
|
| 115 |
"pytest",
|
| 116 |
str(CT.relative_to(ROOT)),
|
|
@@ -130,7 +141,7 @@ def main() -> int:
|
|
| 130 |
"schema_version": 1,
|
| 131 |
"preregistration_id": p["preregistration_id"],
|
| 132 |
"supersedes": "PREREG-S1-TOPO-REFERENCE-R1",
|
| 133 |
-
"freeze_date":
|
| 134 |
"freeze_commit": git("rev-parse", "HEAD"),
|
| 135 |
"preregistration_sha256_before_status_update": sha(P),
|
| 136 |
"reference_data_created": False,
|
|
@@ -203,7 +214,11 @@ def main() -> int:
|
|
| 203 |
"authorization_rule": p["execution_authorization_requirements"],
|
| 204 |
"note": "R2 corrects the pre-DATA R1 mapping ambiguity. Freeze fixes the scientific contract only; separate explicit human execution authorization remains mandatory.",
|
| 205 |
}
|
| 206 |
-
F.write_text(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
p["status"] = "FROZEN_BEFORE_REFERENCE_EXECUTION_AUTHORIZATION"
|
| 208 |
p["execution_authorized"] = False
|
| 209 |
p["freeze_authorization"] = {
|
|
@@ -216,7 +231,11 @@ def main() -> int:
|
|
| 216 |
p["freeze_semantics"] = (
|
| 217 |
"R2 scientific contract is frozen. Reference DATA remain forbidden until a separate explicit human execution-authorization record binds this freeze commit and runner hash."
|
| 218 |
)
|
| 219 |
-
P.write_text(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
print(json.dumps({"status": p["status"], "execution_authorized": False}))
|
| 221 |
return 0
|
| 222 |
|
|
|
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
+
import argparse
|
| 7 |
import hashlib
|
| 8 |
import json
|
| 9 |
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
from datetime import date
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
def main() -> int:
|
| 81 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"--freeze-date",
|
| 84 |
+
type=date.fromisoformat,
|
| 85 |
+
required=True,
|
| 86 |
+
help="Actual freeze date (YYYY-MM-DD); never inherit the draft's date",
|
| 87 |
+
)
|
| 88 |
+
args = parser.parse_args()
|
| 89 |
if F.exists():
|
| 90 |
raise FileExistsError(F)
|
| 91 |
if git("status", "--porcelain"):
|
|
|
|
| 117 |
or proto.get("framework_version") != "2.10.1"
|
| 118 |
):
|
| 119 |
raise RuntimeError("R2 protocol mismatch")
|
| 120 |
+
subprocess.run([sys.executable, str(IND.relative_to(ROOT))], cwd=ROOT, check=True)
|
| 121 |
+
subprocess.run([sys.executable, str(SEED.relative_to(ROOT))], cwd=ROOT, check=True)
|
| 122 |
subprocess.run(
|
| 123 |
[
|
| 124 |
+
sys.executable,
|
| 125 |
"-m",
|
| 126 |
"pytest",
|
| 127 |
str(CT.relative_to(ROOT)),
|
|
|
|
| 141 |
"schema_version": 1,
|
| 142 |
"preregistration_id": p["preregistration_id"],
|
| 143 |
"supersedes": "PREREG-S1-TOPO-REFERENCE-R1",
|
| 144 |
+
"freeze_date": args.freeze_date.isoformat(),
|
| 145 |
"freeze_commit": git("rev-parse", "HEAD"),
|
| 146 |
"preregistration_sha256_before_status_update": sha(P),
|
| 147 |
"reference_data_created": False,
|
|
|
|
| 214 |
"authorization_rule": p["execution_authorization_requirements"],
|
| 215 |
"note": "R2 corrects the pre-DATA R1 mapping ambiguity. Freeze fixes the scientific contract only; separate explicit human execution authorization remains mandatory.",
|
| 216 |
}
|
| 217 |
+
F.write_text(
|
| 218 |
+
json.dumps(freeze, indent=2, sort_keys=True) + "\n",
|
| 219 |
+
encoding="utf-8",
|
| 220 |
+
newline="\n",
|
| 221 |
+
)
|
| 222 |
p["status"] = "FROZEN_BEFORE_REFERENCE_EXECUTION_AUTHORIZATION"
|
| 223 |
p["execution_authorized"] = False
|
| 224 |
p["freeze_authorization"] = {
|
|
|
|
| 231 |
p["freeze_semantics"] = (
|
| 232 |
"R2 scientific contract is frozen. Reference DATA remain forbidden until a separate explicit human execution-authorization record binds this freeze commit and runner hash."
|
| 233 |
)
|
| 234 |
+
P.write_text(
|
| 235 |
+
json.dumps(p, indent=2, ensure_ascii=False) + "\n",
|
| 236 |
+
encoding="utf-8",
|
| 237 |
+
newline="\n",
|
| 238 |
+
)
|
| 239 |
print(json.dumps({"status": p["status"], "execution_authorized": False}))
|
| 240 |
return 0
|
| 241 |
|
scripts/publication_empirical.py
CHANGED
|
@@ -34,7 +34,7 @@ def save(path: Path, value: object) -> None:
|
|
| 34 |
|
| 35 |
def inventory(path: Path) -> dict[str, str]:
|
| 36 |
return {
|
| 37 |
-
|
| 38 |
for p in sorted(path.rglob("*"))
|
| 39 |
if p.is_file() and p.name != "ARTIFACTS.json"
|
| 40 |
}
|
|
|
|
| 34 |
|
| 35 |
def inventory(path: Path) -> dict[str, str]:
|
| 36 |
return {
|
| 37 |
+
p.relative_to(path).as_posix(): sha(p)
|
| 38 |
for p in sorted(path.rglob("*"))
|
| 39 |
if p.is_file() and p.name != "ARTIFACTS.json"
|
| 40 |
}
|
scripts/run_emb001b_unseen_v4.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run frozen exploratory V4 to an external, new output directory."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import datetime
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import math
|
| 10 |
+
import os
|
| 11 |
+
import platform
|
| 12 |
+
import statistics
|
| 13 |
+
import subprocess
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
from dataclasses import asdict
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 20 |
+
sys.path.insert(0, str(ROOT))
|
| 21 |
+
|
| 22 |
+
import yaml # noqa: E402
|
| 23 |
+
|
| 24 |
+
from src.research.connectome_embodiment import _simulate # noqa: E402
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def save(path, value):
|
| 28 |
+
path.write_text(
|
| 29 |
+
json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n",
|
| 30 |
+
encoding="utf-8",
|
| 31 |
+
newline="\n",
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def git(*args):
|
| 36 |
+
return (
|
| 37 |
+
subprocess.check_output(
|
| 38 |
+
["git", "-c", f"safe.directory={ROOT.as_posix()}", *args], cwd=ROOT
|
| 39 |
+
)
|
| 40 |
+
.decode()
|
| 41 |
+
.strip()
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def sha(path):
|
| 46 |
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def main():
|
| 50 |
+
parser = argparse.ArgumentParser()
|
| 51 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 52 |
+
out = parser.parse_args().output.resolve()
|
| 53 |
+
if out.is_relative_to(ROOT):
|
| 54 |
+
raise ValueError("Output must be outside the tested checkout")
|
| 55 |
+
if git("status", "--porcelain"):
|
| 56 |
+
raise ValueError("Clean committed source required")
|
| 57 |
+
p = ROOT / "research/preregistrations/PREREG-EMB001B-UNSEEN-V4.json"
|
| 58 |
+
protocol = json.loads(p.read_text())
|
| 59 |
+
config_path = ROOT / "configs/learning_experiment.yaml"
|
| 60 |
+
config = yaml.safe_load(config_path.read_text())
|
| 61 |
+
out.mkdir(parents=True, exist_ok=False)
|
| 62 |
+
save(
|
| 63 |
+
out / "freeze.json",
|
| 64 |
+
{
|
| 65 |
+
"commit": git("rev-parse", "HEAD"),
|
| 66 |
+
"tree": git("rev-parse", "HEAD^{tree}"),
|
| 67 |
+
"utc_before_execution": datetime.datetime.now(
|
| 68 |
+
datetime.timezone.utc
|
| 69 |
+
).isoformat(),
|
| 70 |
+
"python": sys.version,
|
| 71 |
+
"platform": platform.platform(),
|
| 72 |
+
"command": [sys.executable, *sys.argv],
|
| 73 |
+
"PYTHONHASHSEED": os.environ.get("PYTHONHASHSEED"),
|
| 74 |
+
"protocol": protocol,
|
| 75 |
+
"status_before": "",
|
| 76 |
+
"hashes": {
|
| 77 |
+
str(f.relative_to(ROOT)): sha(f)
|
| 78 |
+
for f in [
|
| 79 |
+
p,
|
| 80 |
+
config_path,
|
| 81 |
+
Path(__file__),
|
| 82 |
+
*sorted((ROOT / "src").rglob("*.py")),
|
| 83 |
+
]
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
)
|
| 87 |
+
(out / "pip-freeze.txt").write_text(
|
| 88 |
+
subprocess.check_output(
|
| 89 |
+
[sys.executable, "-m", "pip", "freeze", "--all"]
|
| 90 |
+
).decode(),
|
| 91 |
+
encoding="utf-8",
|
| 92 |
+
newline="\n",
|
| 93 |
+
)
|
| 94 |
+
results, integrity = [], []
|
| 95 |
+
started = time.monotonic()
|
| 96 |
+
for seed in protocol["seeds"]:
|
| 97 |
+
ticks = protocol["ticks"]
|
| 98 |
+
schedule = tuple(
|
| 99 |
+
(0.003 if seed % 2 == 0 else -0.003) if 250 <= t < 500 else 0.0
|
| 100 |
+
for t in range(ticks)
|
| 101 |
+
)
|
| 102 |
+
donor = _simulate(config, seed, ticks, "closed_loop", perturb=False)
|
| 103 |
+
save(out / f"donor-{seed}.json", asdict(donor))
|
| 104 |
+
runs = {
|
| 105 |
+
"donor_unperturbed": donor,
|
| 106 |
+
"yoked_unperturbed": _simulate(
|
| 107 |
+
config, seed, ticks, "yoked_replay", donor=donor, perturb=False
|
| 108 |
+
),
|
| 109 |
+
"closed_challenge": _simulate(
|
| 110 |
+
config, seed, ticks, "closed_loop", external_torque_schedule=schedule
|
| 111 |
+
),
|
| 112 |
+
"yoked_challenge": _simulate(
|
| 113 |
+
config,
|
| 114 |
+
seed,
|
| 115 |
+
ticks,
|
| 116 |
+
"yoked_replay",
|
| 117 |
+
donor=donor,
|
| 118 |
+
external_torque_schedule=schedule,
|
| 119 |
+
),
|
| 120 |
+
"feedback_absent_challenge": _simulate(
|
| 121 |
+
config,
|
| 122 |
+
seed,
|
| 123 |
+
ticks,
|
| 124 |
+
"feedback_absent",
|
| 125 |
+
external_torque_schedule=schedule,
|
| 126 |
+
),
|
| 127 |
+
"pd_challenge": _simulate(
|
| 128 |
+
config,
|
| 129 |
+
seed,
|
| 130 |
+
ticks,
|
| 131 |
+
"controller_only",
|
| 132 |
+
external_torque_schedule=schedule,
|
| 133 |
+
),
|
| 134 |
+
}
|
| 135 |
+
y = runs["yoked_challenge"]
|
| 136 |
+
checks = {
|
| 137 |
+
"complete": all(
|
| 138 |
+
r.error is None and r.metrics["ticks_executed"] == ticks
|
| 139 |
+
for r in runs.values()
|
| 140 |
+
),
|
| 141 |
+
"initial_neural_equal": len({r.before for r in runs.values()}) == 1,
|
| 142 |
+
"initial_body_equal": all(
|
| 143 |
+
r.metrics["gateway_state_before"]
|
| 144 |
+
== donor.metrics["gateway_state_before"]
|
| 145 |
+
for r in runs.values()
|
| 146 |
+
),
|
| 147 |
+
"graph_equal": all(
|
| 148 |
+
r.metrics["graph"] == donor.metrics["graph"] for r in runs.values()
|
| 149 |
+
),
|
| 150 |
+
"sentinel_exact": runs["yoked_unperturbed"].metrics["trace"]
|
| 151 |
+
== donor.metrics["trace"],
|
| 152 |
+
"donor_observations_exact": [
|
| 153 |
+
(r["observed_q_rad"], r["observed_omega_rad_s"])
|
| 154 |
+
for r in y.metrics["trace"]
|
| 155 |
+
]
|
| 156 |
+
== donor.sensor_tape,
|
| 157 |
+
"yoked_commands_unchanged": y.motor_tape == donor.motor_tape,
|
| 158 |
+
"body_responds_to_disturbance": y.sensor_tape != donor.sensor_tape,
|
| 159 |
+
"absent_zero": all(
|
| 160 |
+
r["observed_q_rad"] == r["observed_omega_rad_s"] == 0
|
| 161 |
+
for r in runs["feedback_absent_challenge"].metrics["trace"]
|
| 162 |
+
),
|
| 163 |
+
}
|
| 164 |
+
integrity.append({"seed": seed, "checks": checks})
|
| 165 |
+
for condition, run in runs.items():
|
| 166 |
+
trace = run.metrics["trace"][250:]
|
| 167 |
+
rmse = math.sqrt(
|
| 168 |
+
statistics.mean((r["target_rad"] - r["q_rad"]) ** 2 for r in trace)
|
| 169 |
+
)
|
| 170 |
+
results.append(
|
| 171 |
+
{
|
| 172 |
+
"seed": seed,
|
| 173 |
+
"condition": condition,
|
| 174 |
+
"post_disturbance_rmse": rmse,
|
| 175 |
+
"full_rmse": run.metrics["tracking_rmse_rad"],
|
| 176 |
+
"error": run.error,
|
| 177 |
+
}
|
| 178 |
+
)
|
| 179 |
+
save(
|
| 180 |
+
out / f"runs-{seed}.json",
|
| 181 |
+
{"schedule": schedule, "runs": {k: asdict(v) for k, v in runs.items()}},
|
| 182 |
+
)
|
| 183 |
+
save(out / "integrity.json", integrity)
|
| 184 |
+
save(out / "outcomes.json", results)
|
| 185 |
+
if (
|
| 186 |
+
time.monotonic() - started > 600
|
| 187 |
+
or sum(f.stat().st_size for f in out.iterdir()) > 100000000
|
| 188 |
+
):
|
| 189 |
+
raise RuntimeError(
|
| 190 |
+
"Frozen resource ceiling exceeded; partial outputs retained"
|
| 191 |
+
)
|
| 192 |
+
differences = []
|
| 193 |
+
for seed in protocol["seeds"]:
|
| 194 |
+
rows = {r["condition"]: r for r in results if r["seed"] == seed}
|
| 195 |
+
differences.append(
|
| 196 |
+
{
|
| 197 |
+
"seed": seed,
|
| 198 |
+
"difference": rows["yoked_challenge"]["post_disturbance_rmse"]
|
| 199 |
+
- rows["closed_challenge"]["post_disturbance_rmse"],
|
| 200 |
+
}
|
| 201 |
+
)
|
| 202 |
+
summary = {}
|
| 203 |
+
for name, selected in (
|
| 204 |
+
("pooled", differences),
|
| 205 |
+
("positive_torque", [r for r in differences if r["seed"] % 2 == 0]),
|
| 206 |
+
("negative_torque", [r for r in differences if r["seed"] % 2]),
|
| 207 |
+
):
|
| 208 |
+
values = [r["difference"] for r in selected]
|
| 209 |
+
summary[name] = {
|
| 210 |
+
"n": len(values),
|
| 211 |
+
"mean": statistics.mean(values),
|
| 212 |
+
"median": statistics.median(values),
|
| 213 |
+
"min": min(values),
|
| 214 |
+
"max": max(values),
|
| 215 |
+
"positive": sum(v > 0 for v in values),
|
| 216 |
+
"zero": values.count(0),
|
| 217 |
+
"negative": sum(v < 0 for v in values),
|
| 218 |
+
}
|
| 219 |
+
valid = all(all(row["checks"].values()) for row in integrity) and not git(
|
| 220 |
+
"status", "--porcelain"
|
| 221 |
+
)
|
| 222 |
+
save(
|
| 223 |
+
out / "result.json",
|
| 224 |
+
{
|
| 225 |
+
"valid": valid,
|
| 226 |
+
"status_after": git("status", "--porcelain"),
|
| 227 |
+
"paired_differences": differences,
|
| 228 |
+
"summary": summary,
|
| 229 |
+
"scientific_evidence": False,
|
| 230 |
+
},
|
| 231 |
+
)
|
| 232 |
+
save(out / "sha256sums.json", {f.name: sha(f) for f in sorted(out.iterdir())})
|
| 233 |
+
print(json.dumps({"valid": valid, "summary": summary}, indent=2))
|
| 234 |
+
return 0 if valid else 1
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
raise SystemExit(main())
|
scripts/run_stage1_reference_topology_mapping_r2.py
CHANGED
|
@@ -6,9 +6,12 @@ from __future__ import annotations
|
|
| 6 |
import hashlib
|
| 7 |
import json
|
| 8 |
import random
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import Any
|
| 11 |
|
|
|
|
|
|
|
| 12 |
from reference.stage1_topology_brian2 import runner_r2 as reference
|
| 13 |
from scripts.run_stage1_topology_v2 import (
|
| 14 |
canonical_coords,
|
|
@@ -90,7 +93,9 @@ def main() -> int:
|
|
| 90 |
}
|
| 91 |
OUT.parent.mkdir(parents=True, exist_ok=True)
|
| 92 |
OUT.write_text(
|
| 93 |
-
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
|
|
|
|
|
| 94 |
)
|
| 95 |
print(
|
| 96 |
json.dumps(
|
|
|
|
| 6 |
import hashlib
|
| 7 |
import json
|
| 8 |
import random
|
| 9 |
+
import sys
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 14 |
+
|
| 15 |
from reference.stage1_topology_brian2 import runner_r2 as reference
|
| 16 |
from scripts.run_stage1_topology_v2 import (
|
| 17 |
canonical_coords,
|
|
|
|
| 93 |
}
|
| 94 |
OUT.parent.mkdir(parents=True, exist_ok=True)
|
| 95 |
OUT.write_text(
|
| 96 |
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
| 97 |
+
encoding="utf-8",
|
| 98 |
+
newline="\n",
|
| 99 |
)
|
| 100 |
print(
|
| 101 |
json.dumps(
|
scripts/verify_determinism_reproduction.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify archived reproduction bytes without modifying or rerunning the study."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main() -> None:
|
| 9 |
+
report = (
|
| 10 |
+
Path(__file__).resolve().parents[1]
|
| 11 |
+
/ "research/reviews/DETERMINISM_REPRODUCTION_20261024"
|
| 12 |
+
)
|
| 13 |
+
artifacts = report / "artifacts"
|
| 14 |
+
checksums = json.loads((artifacts / "sha256sums.json").read_text(encoding="utf-8"))
|
| 15 |
+
for name, expected in checksums.items():
|
| 16 |
+
target = artifacts / name
|
| 17 |
+
if target.resolve().parent != artifacts.resolve():
|
| 18 |
+
raise ValueError(f"Invalid artifact path: {name}")
|
| 19 |
+
if hashlib.sha256(target.read_bytes()).hexdigest() != expected:
|
| 20 |
+
raise ValueError(f"Reproduction artifact changed: {name}")
|
| 21 |
+
freeze = json.loads((artifacts / "freeze.json").read_text(encoding="utf-8"))
|
| 22 |
+
actual = hashlib.sha256((report / "reproduce.py").read_bytes()).hexdigest()
|
| 23 |
+
if actual != freeze["harness_sha256"]:
|
| 24 |
+
raise ValueError("Executed reproduction harness changed")
|
| 25 |
+
print(f"PASS: {len(checksums)} immutable artifacts and executed harness")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if __name__ == "__main__":
|
| 29 |
+
main()
|
scripts/verify_emb001b_unseen_v4.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify archived V4 bytes and recompute the primary outcome from traces."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import statistics
|
| 7 |
+
import subprocess
|
| 8 |
+
import zipfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
OUT = ROOT / "research/experiments/EXP-EMB001B-UNSEEN-V4-20261024"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
archive = json.loads((OUT / "archive-index.json").read_text())
|
| 17 |
+
assert (
|
| 18 |
+
hashlib.sha256((OUT / archive["archive"]).read_bytes()).hexdigest()
|
| 19 |
+
== archive["sha256"]
|
| 20 |
+
)
|
| 21 |
+
freeze = json.loads((OUT / "freeze.json").read_text())
|
| 22 |
+
git = ["git", "-c", f"safe.directory={ROOT.as_posix()}"]
|
| 23 |
+
for name, expected in freeze["hashes"].items():
|
| 24 |
+
name = name.replace("\\", "/")
|
| 25 |
+
blob = subprocess.check_output(
|
| 26 |
+
git + ["show", freeze["commit"] + ":" + name], cwd=ROOT
|
| 27 |
+
)
|
| 28 |
+
assert hashlib.sha256(blob).hexdigest() == expected, name
|
| 29 |
+
outcomes = json.loads((OUT / "outcomes.json").read_text())
|
| 30 |
+
assert len(outcomes) == 120
|
| 31 |
+
differences = []
|
| 32 |
+
with zipfile.ZipFile(OUT / archive["archive"]) as z:
|
| 33 |
+
for name, expected in json.loads((OUT / "sha256sums.json").read_text()).items():
|
| 34 |
+
data = (
|
| 35 |
+
z.read(name)
|
| 36 |
+
if name in archive["members"]
|
| 37 |
+
else (OUT / name).read_bytes()
|
| 38 |
+
)
|
| 39 |
+
assert hashlib.sha256(data).hexdigest() == expected, name
|
| 40 |
+
for seed in freeze["protocol"]["seeds"]:
|
| 41 |
+
raw = json.loads(z.read(f"runs-{seed}.json"))
|
| 42 |
+
assert set(raw["runs"]) == set(freeze["protocol"]["conditions"])
|
| 43 |
+
donor = raw["runs"]["donor_unperturbed"]
|
| 44 |
+
assert donor == json.loads(z.read(f"donor-{seed}.json"))
|
| 45 |
+
assert raw["schedule"] == [
|
| 46 |
+
(0.003 if seed % 2 == 0 else -0.003) if 250 <= t < 500 else 0.0
|
| 47 |
+
for t in range(1000)
|
| 48 |
+
]
|
| 49 |
+
values = {}
|
| 50 |
+
for condition, run in raw["runs"].items():
|
| 51 |
+
trace = run["metrics"]["trace"]
|
| 52 |
+
assert run["error"] is None and len(trace) == 1000
|
| 53 |
+
assert run["before"] == donor["before"]
|
| 54 |
+
assert run["metrics"]["graph"] == donor["metrics"]["graph"]
|
| 55 |
+
assert (
|
| 56 |
+
run["metrics"]["gateway_state_before"]
|
| 57 |
+
== donor["metrics"]["gateway_state_before"]
|
| 58 |
+
)
|
| 59 |
+
value = math.sqrt(
|
| 60 |
+
statistics.mean(
|
| 61 |
+
(r["target_rad"] - r["q_rad"]) ** 2 for r in trace[250:]
|
| 62 |
+
)
|
| 63 |
+
)
|
| 64 |
+
row = next(
|
| 65 |
+
r
|
| 66 |
+
for r in outcomes
|
| 67 |
+
if r["seed"] == seed and r["condition"] == condition
|
| 68 |
+
)
|
| 69 |
+
assert value == row["post_disturbance_rmse"]
|
| 70 |
+
values[condition] = value
|
| 71 |
+
yoke = raw["runs"]["yoked_challenge"]
|
| 72 |
+
assert (
|
| 73 |
+
raw["runs"]["yoked_unperturbed"]["metrics"]["trace"]
|
| 74 |
+
== donor["metrics"]["trace"]
|
| 75 |
+
)
|
| 76 |
+
assert [
|
| 77 |
+
[r["observed_q_rad"], r["observed_omega_rad_s"]]
|
| 78 |
+
for r in yoke["metrics"]["trace"]
|
| 79 |
+
] == donor["sensor_tape"]
|
| 80 |
+
assert yoke["motor_tape"] == donor["motor_tape"]
|
| 81 |
+
assert yoke["sensor_tape"] != donor["sensor_tape"]
|
| 82 |
+
assert all(
|
| 83 |
+
r["observed_q_rad"] == r["observed_omega_rad_s"] == 0
|
| 84 |
+
for r in raw["runs"]["feedback_absent_challenge"]["metrics"]["trace"]
|
| 85 |
+
)
|
| 86 |
+
differences.append(
|
| 87 |
+
{
|
| 88 |
+
"seed": seed,
|
| 89 |
+
"difference": values["yoked_challenge"]
|
| 90 |
+
- values["closed_challenge"],
|
| 91 |
+
}
|
| 92 |
+
)
|
| 93 |
+
result = json.loads((OUT / "result.json").read_text())
|
| 94 |
+
assert result["valid"] and result["status_after"] == ""
|
| 95 |
+
assert differences == result["paired_differences"]
|
| 96 |
+
assert (
|
| 97 |
+
statistics.mean(r["difference"] for r in differences)
|
| 98 |
+
== result["summary"]["pooled"]["mean"]
|
| 99 |
+
)
|
| 100 |
+
print(
|
| 101 |
+
"PASS: source bindings, archived hashes, 120 raw runs, causal controls and paired primary outcomes"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
scripts/verify_emb002a_delay_sweep_v4.py
CHANGED
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
| 5 |
|
| 6 |
import hashlib
|
| 7 |
import json
|
|
|
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Any
|
| 10 |
|
|
@@ -100,9 +102,16 @@ def main() -> int:
|
|
| 100 |
assert hashes["preregistration"] == sha256(PREREG)
|
| 101 |
assert hashes["evaluation_data"] == sha256(OUT / "data/evaluation.json")
|
| 102 |
assert hashes["statistics"] == sha256(OUT / "analysis/statistics.json")
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
for line in (OUT / "checksums.sha256").read_text(encoding="utf-8").splitlines():
|
| 108 |
digest, relpath = line.split(" ", 1)
|
|
|
|
| 5 |
|
| 6 |
import hashlib
|
| 7 |
import json
|
| 8 |
+
import re
|
| 9 |
+
import subprocess
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any
|
| 12 |
|
|
|
|
| 102 |
assert hashes["preregistration"] == sha256(PREREG)
|
| 103 |
assert hashes["evaluation_data"] == sha256(OUT / "data/evaluation.json")
|
| 104 |
assert hashes["statistics"] == sha256(OUT / "analysis/statistics.json")
|
| 105 |
+
# Persisted DATA bind the executed commit, not later runtime development.
|
| 106 |
+
source_commit = manifest["source_freeze"]["commit"]
|
| 107 |
+
assert re.fullmatch(r"[0-9a-f]{40}", source_commit)
|
| 108 |
+
source = subprocess.run(
|
| 109 |
+
["git", "show", f"{source_commit}:src/research/connectome_embodiment.py"],
|
| 110 |
+
cwd=ROOT,
|
| 111 |
+
check=True,
|
| 112 |
+
capture_output=True,
|
| 113 |
+
).stdout
|
| 114 |
+
assert hashes["canonical_embodiment_source"] == hashlib.sha256(source).hexdigest()
|
| 115 |
|
| 116 |
for line in (OUT / "checksums.sha256").read_text(encoding="utf-8").splitlines():
|
| 117 |
digest, relpath = line.split(" ", 1)
|
scripts/verify_stage1_reference_dynamics_reuse_r2.py
CHANGED
|
@@ -120,7 +120,9 @@ def main() -> int:
|
|
| 120 |
}
|
| 121 |
OUT.parent.mkdir(parents=True, exist_ok=True)
|
| 122 |
OUT.write_text(
|
| 123 |
-
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
print(
|
| 126 |
json.dumps(
|
|
|
|
| 120 |
}
|
| 121 |
OUT.parent.mkdir(parents=True, exist_ok=True)
|
| 122 |
OUT.write_text(
|
| 123 |
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
| 124 |
+
encoding="utf-8",
|
| 125 |
+
newline="\n",
|
| 126 |
)
|
| 127 |
print(
|
| 128 |
json.dumps(
|