Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

### Fixed
- Per-run CSV and PAE plot reuse no longer depend on the device number of the files. A shared filesystem such as BeeGFS reports another `st_dev` for the same file on each cluster node, so a recursive run on one node recomputed every table and redrew every plot certified on another. Size, mtime, ctime and inode still detect edits and replaced files. Manifests written by 1.4.4 are recomputed once, as after any software change.

## 1.4.4 - 2026-10-01

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion src/alphajudge/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ def file_fingerprint(path: Path) -> dict[str, int]:

ctime supplements mtime, but coarse/preserved timestamps can still hide
same-size rewrites. Use content validation when metadata is insufficient.
The device number is not part of it: a shared filesystem (BeeGFS) reports
another st_dev for the same file on each cluster node.
"""
stat = path.stat()
return {"size": stat.st_size, "mtime_ns": stat.st_mtime_ns, "ctime_ns": stat.st_ctime_ns,
"device": stat.st_dev, "inode": stat.st_ino}
"inode": stat.st_ino}


def software_identity() -> dict:
Expand Down
19 changes: 19 additions & 0 deletions test/test_run_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import shutil
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace

import numpy as np
import pytest
Expand Down Expand Up @@ -232,6 +233,24 @@ def test_cache_detects_added_removed_and_replaced_inputs(af2_run):
assert cache.request_identity(directory, csv_path) != before


def test_cache_and_plots_are_reused_on_a_node_with_another_device_number(af2_run, record_process, monkeypatch):
directory, _ = af2_run
score(directory, skip_pae_png=False)
calls = record_process()
stat = Path.stat
def on_another_node(self, **kwargs):
fields = {name: getattr(stat(self, **kwargs), name)
for name in dir(os.stat_result) if name.startswith("st_")}
fields["st_dev"] += 1
return SimpleNamespace(**fields)
def redraw(*args, **kwargs):
pytest.fail("A current PAE plot was redrawn")
monkeypatch.setattr(Path, "stat", on_another_node)
monkeypatch.setattr(AF2Parser, "parse_run", redraw)
score(directory, skip_pae_png=False)
assert calls == []


def test_png_toggle_generates_missing_plot_without_rescoring(af2_run, monkeypatch):
directory, models = af2_run
original = score(directory)
Expand Down
Loading