Repository navigation
Fix the rc4 external review: mount points, hard links, commit fsync, content verification - #140
Conversation
…t hard-linked files (B1, I1) A Linux bind mount below the experiment is an ordinary folder to the walk, so discovery read files from outside the selected folder. LinkGuard now refuses any folder (or file) below the walked root that is a mount point, with the error mounted_folder naming it: a path listed in /proc/self/mountinfo (the only way to see a same-device bind mount; os.path.ismount and st_dev report an ordinary folder), or a folder on another device than the root. The rule holds inside OME-Zarr stores, the masks folder, check, run, convert and RunInputs (mounted_folder is one of LINK_CODES). The root itself may be on any mount. An image or mask whose link count is above 1 is the warning hard_linked_file (from the stat claim() already takes); reading and fingerprints are unchanged. Tests: a real bind mount (skipped by name without mount --bind as root) and a replaced mount table, which always runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
_commit() replaced the manifest without an fsync of the output folder; the flush came only from _finish_committed. The folder is now flushed in _commit, right after os.replace, so the commit is on the disk when it returns. The flush a recovering holder of the lock needs moved to finish_publication. A test records the order of renames and folder flushes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…fication (I3) (a) The contract, user guide, AGENTS.md, CHANGELOG and docstrings (manifest, fingerprint, inventory, run_experiment, RunInputs) now name the identity rule for reuse, resume and RunInputs: the metadata fingerprint (size and mtime_ns; for OME-Zarr the inventory). They also name its known blind spot, a same-size rewrite with the modification time set back. (b) Opt-in strict reuse: RunMode(name, verify_content=True) for run_experiment(mode=...), CLI --verify-content / --no-verify-content, and the notebook's "Verify each image's content" box. Each image's content SHA-256 (TIFF bytes; OME-Zarr: per-file SHA-256s over the files its inventory covers) is recorded in files[].content_sha256 and joins the row key, so earlier and staged rows are reused only for the same content. An image read must keep it after the read (else source_changed_during_run). RunInputs.open of such a run checks it once. settings.verify_content is a recorded input: turning it on or off recomputes every row, is reported as an input change, and discards staged rows of the other mode with the reason. The default run is unchanged. RunMode carries the flag through mode=, because run_experiment's parameter count is at its shrink-only baseline. package_versions moved to versions.py (re-exported) to keep manifest.py under 600 lines. New code: mounts-free source_content.py and inventory.store_files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…ape (review of #140) - mounts.MountCheck read st_dev from os.DirEntry.stat(), which is 0 on Windows, so every folder of every experiment was refused there (637 failures on the Windows CI jobs). It now uses os.stat(entry.path); regression test with a Windows-like entry. - settings.verify_content is written only when true, so a default run's manifest keeps the v0.4.0rc4 keys (contract and changelog say so). - Tests for a bind-mounted file inside an OME-Zarr store and for a strict-run image that cannot be hashed (never read), which mutations showed untested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
|
Independent review, merged as 544fa45 (merge commit). Fixes are in 4d0726c. Blocking, fixed: Minor, fixed: I checked these and found no defect:
Gate:
Generated by Claude Code |
Q66 confirmed by the owner (3 November 2026); Q73's follow-up decided: neither a stated reshape nor a reorder in v0.4, kept in issue #144; Q74 records the tie-break and the consumers checked. The changelog gains a section for rc5 (PRs #139, #140, #142, #143) with migration notes: mounts refused, position_id required in v2, micrometre conversion within 1 ULP, --verify-content, and the tie-break. The migration guide gets the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
This fixes the findings of the external rc4 review. The owner's decisions are final. Do not merge before the independent review.
B1: mount points below the experiment or masks folder are refused
mounts.MountCheck.LinkGuard.real()now refuses any folder (or file) below the walked root that is a mount point, with the errormounted_foldernaming it. A path counts as a mount point when:/proc/self/mountinfo(octal escapes undone), orst_devdiffers from the root's.mounted_folderis inLINK_CODES, so discovery, OME-Zarr stores (check_store), the masks folder,check,run,convertandRunInputsall refuse it the same way.runwrites no table or manifest.os.path.ismount()returns False andst_devis the same as the parent's (only the inode differs). That is why the mountinfo check is needed.tests/test_mounts.py:mount --bind(skipped by name without root or permission). Here it ran and passed;st_devrule;mountinfo.mounts.py):/proc/self/mountinfocannot be read (macOS, BSD, Windows), only a mount on another device is seen, so a BSDnullfsmount of the same device is missed;bindfs(FUSE) and Windows mounted folders have another device, and a Windows mounted folder is also a mount-point reparse point, which the junction/link rule already handles;st_devand are refused too;stat, including the folders inside OME-Zarr stores (their files are not stat'ed).I1: hard-linked images and masks get a warning
claim()uses thestatit already takes. A file withst_nlink > 1is the warninghard_linked_file, naming each file and its count. Reading and fingerprints are unchanged.checkstill exits 0 (tested).I2: the output folder is fsynced after the commit
_commit()now calls_flush_folder(output)right after the manifest'sos.replace. Before, only_finish_committedflushed.finish_publication, so a normal run flushes once.I3: identity rule stated; opt-in
--verify-contentmtime_ns; for OME-Zarr, the inventory). They also state its blind spot: a rewrite of the same size with the modification time set back. A test shows the blind spot in the default mode.RunMode(name, verify_content=True)passed asrun_experiment(mode=...);--verify-content/--no-verify-content;files[].content_sha256(TIFF bytes; OME-Zarr: per-file SHA-256s over the files the inventory covers) andsettings.verify_content;RowKeyand the staging key, so earlier and staged rows are reused only for the same content;source_changed_during_run);unreadable_planes;RunInputs.openof such a run check every image's content once (it already checked fingerprints).verify_contentis a recorded input: a run that does not give it keeps the recorded choice;verify_content;mode=(RunMode) instead of a new parameter, becauserun_experimentis at its shrink-only baseline of 15 parameters.manifest.pyunder 600 lines,package_versionsmoved toversions.py(still re-exported frommanifest).source_content.py,versions.py,mounts.py, plusinventory.store_files. All have declared layers.RunInputs.open.Also
Opened #141: "Conversion reopens TIFF headers for pixel reads and memory checks (performance follow-up of #96)".
Gate (Python 3.13,
.[dev,notebook,zarr])ruff check .: pass.ruff check --ignore-noqa .reports the same diagnostics on main and on this branch: no new suppressed violations.ruff format --check .: pass.python tools/check.py --rules: pass. The baseline is unchanged;tools/code_rules.pySHA starts with cc04a420.pytest: 2090 passed, 4 skipped, 0 failed. The skips are three case-sensitive file-system tests and one permissions-as-root test. The real bind-mount tests ran (not skipped).🤖 Generated with Claude Code
https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc