Repository navigation
PP-12: staging and resume, crash-consistent publication, OME-Zarr inventory fingerprint (#86, #90) - #132
Merged
Conversation
… only) manifest.py re-exports source_fingerprint; content_hash imports LABELS_GROUP from the new module. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
The fingerprint of an OME-Zarr folder was the total size of its files and the newest time, so a chunk rewritten with the same size whose new time stayed below another file's (or a chunk removed and another of the same size added) left it unchanged and a restart reused rows of pixels that no longer exist. inventory.source_signature now also gives the SHA-256 of the folder's inventory (each file's relative path, size and mtime, sorted by path; still metadata only, design v0.4 §1). Fingerprint gains 'inventory', FileEntry 'inventory_sha256' (written for OME-Zarr images only), the mask and conversion records keep it for folders, and check_sources compares it. An image recorded without one (v0.4.0rc3) counts as changed. A folder that cannot be listed now raises instead of being skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…ion) copy_changed_after_conversion (a copy changed, gone or added since the conversion record was written) is now an error: the run stops after the header checks, before any pixel is read or anything but the report is written, since RunInputs would refuse the run anyway. The message says to convert the originals again into a new folder. conversion_source_changed stays a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…rces checked during the run (#90) Staging and resume (design v0.4 §1): each image's rows are staged in .staging as soon as it is read (rows file, then a record with its staging key: row key, pixel type, schema version, the two packages' versions). The next run reuses staged rows only when the key and fingerprint still match; anything else is discarded and reported (report 'resumed' and 'staged_discarded', RunResult.resumed). A stopped run keeps .staging; a published run removes it. Publication (owner decision 2026-11-02): the generation (table, design, manifest, report) is written into .publish with a publication record, then the manifest's os.replace commits it in one atomic step, then the other files are moved in. Whatever takes the output folder's lock first finishes a committed publication or discards an uncommitted one. RunInputs reads each file by the SHA-256 the manifest names (output folder, or .publish while a publication is unfinished) and reads again if the manifest changed meanwhile. The old backup-and-restore protocol is gone (its tests move to test_publication.py, which kills the run with SIGKILL before each of the 24 steps of a publication and checks the folder). #90: headers are bound to fingerprints (preflight(bind_headers=True)), each image is fingerprinted before and after its pixels are read (only then are its rows staged) and every image again just before the commit; any change is the error source_changed_during_run and nothing is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
The manifest entry now records the fingerprint taken when the header was read (file_entry_with), so a run walks an OME-Zarr image three times for a reused image (header before and after, the check before the commit) and four for an image it reads (plus the check after its pixels are read), instead of four and six. A change between the header and the pixel read is still found by the fingerprint taken after the read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…gerprints Contract §9.4 (new), §8, §9, §9.2, §17; user guide (interrupted runs, the one-step switch, images changing during the run, the two errors); plan §10 PP-12 and decided question 61; development.md and AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…resume # Conflicts: # docs/contract.md # docs/plan.md # tools/code_rules_baseline.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Conflicts resolved hunk by hunk: - docs/plan.md: main's "Before the audit" entry (#82, #83, #98) kept, then PP-12's; main's questions 61 to 63 kept and PP-12's question renumbered 64. - run_inputs.py RunInputs.design(): main's strict read_confirmed_design, on the file PP-12's published_path names (the current generation's design). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
After merging main (#83's strict design read), run_inputs.py was 610 lines, over the 600-line rule. The six text helpers move unchanged to a new pure module; run_inputs.py imports them. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…ows) The test moved a rewritten chunk's time by 1 ns. NTFS keeps times in 100 ns steps, so on Windows the change was rounded away, the chunk looked unchanged and the run published (both Windows CI jobs). 1 ms is kept by every filesystem the CI runs on; the test still fails on main, which never checks during the read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
guijacquemet
marked this pull request as ready for review
November 2, 2026 20:22
os.walk does not enter a link to a folder by default, so a chunk changed behind a linked folder, which the reader follows, left the fingerprint unchanged. Links are now followed and a loop of links is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Another pixel type, or an image changed after its header was read, was reported as changed roles or settings, and unreadable staged rows were reported by the record's hashed file name. The conversion-copy error no longer says nothing was written (its report is, PP-8c). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Conflicts: contract §8 (main's link errors kept; copy_changed_after_conversion moved to the errors, source_changed_during_run added), plan §10 (main's entry kept, PP-12's after it) and §12 (main's questions 64 and 65 kept, PP-12's question renumbered 66, its references updated), run/publish.py (PP-12's: the .run- staging folders are gone). The --log check (#99) now protects .publish and .staging instead of the .run- folders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Conflicts in plan §10 and §12 only: main's entry and question 66 kept, PP-12's question renumbered 67, its references updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Conflict in plan §12 only: main's questions 67 and 68 kept, PP-12's question renumbered 69, its references in §10 updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Contributor
Author
|
Independent review: merged as af8f769 with a merge commit. The branch was auto-deleted and has been restored on f7ccf98. Review fixes (small commits)
Probes, run independently of the PR's tests
§9 figures (files touched)
Gate on the final head
Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PP-12 of the v0.4.0 plan (design v0.4 §1 and §8): per-file staging and resume, crash-consistent publication, and the two source-provenance issues.
Fixes #86, fixes #90
Summary (by module)
inventory(new, io; the move is its own commit):source_fingerprintandLABELS_GROUPmoved out ofmanifest(re-exported).source_signaturereturns size, newest mtime and, for an OME-Zarr folder, the SHA-256 of its inventory: each file's relative path, size andmtime_ns, sorted by path, with the rootlabelsgroup left out. It is still metadata only (design §1). A folder inside the image that cannot be listed now raises. Links to folders inside the image are followed, as the reader follows them, and links that reach one folder twice are refused (review).fingerprint,manifest,mask_record,conversion_record,sources([Sad Catapella B5][Scientific integrity] Aggregate OME-Zarr fingerprint misses changed chunks when newest mtime is unchanged #86):Fingerprint.inventory, plusFileEntry.inventory_sha256, which is written for OME-Zarr images only. Label images and copies keep the digest in their records too, andcheck_sourcescompares it. An entry recorded without it (v0.4.0rc3) counts as changed, so its rows are never reused.file_entry_withbuilds an entry from a fingerprint that was already taken.run.staging(new, io): each image's rows are staged in.stagingas soon as the image is read. The rows go to<id>.csv(the normalization writer), then<id>.jsonnames the image, the rows' SHA-256 and the staging key: theRowKey, the pixel type, the schema version and the versions of preprocessing and normalization.run.resume(new, io): for each file that needs new rows, the run either resumes it (same staging key and fingerprint) or reads it and stages it. All other staged rows are discarded and the reason is reported, by image. Afullrun reuses no staged rows. It also holdssource_changed_during_run([Sad Catapella B5][Scientific integrity] Source files may change between fingerprint capture and histogram publication #90).run.generation(new, io): the publication protocol (below),finish_publication,published_pathandread_stable.run.publish: onlywrite_reportremains. The backup-and-restore protocol (and its.run-folders) is removed.run.pipeline/run.steps: the run's preflight binds each header to its fingerprint (bind_headers=True). The rows come fromresume, and the run stops onsource_changed_during_run. Every image is fingerprinted again inside the publication, just before the commit. The generation is published throughstaged_generation, and.stagingis removed after the commit.run.lock: once the lock is held, an interrupted publication is finished (if committed) or discarded (if not). This applies torun,check,masks,design,calibrateand the notebook's writes.run_inputs: reads the table and the design by the SHA-256 the manifest names (from the output folder, or from.publishwhile a publication is unfinished). It reads again if the manifest changed during the read.log_target(after merging Links stay inside the experiment and masks folders; --log is checked before it opens (#81, #99) #128):--logis refused inside.publishand.staging(the.run-folders no longer exist).discovery_report/RunResult/report/notebook_run: newresumedandstaged_discardedfields in the report,RunResult.resumed, a summary line and a notebook tile.conversion_record(owner decision):copy_changed_after_conversionis now an error. The run stops after the header checks and writes only its report. The message says to convert the originals again into a new folder.run_inputs_text(new, pure; a move-only commit):RunInputs' refusal texts, moved unchanged sorun_inputs.pystays under 600 lines.development.mdandAGENTS.md. Version stays 0.4.0rc3.Decisions
The publication design (guarantee). Manifest-last commit, the simplest design that keeps the documented files as plain files on every system:
.publishand flushed. Thenpublication.jsonis written, with each file's SHA-256 and the files to remove (mask_relationships.json).os.replace(.publish/run_manifest.json, run_manifest.json). Nothing in the output folder changes before this..publishand then.stagingare deleted.What each reader sees:
RunInputs) see exactly one generation, old or new, never a mix, also after a kill at any step.plane_histograms.csvalways reads one whole table of one generation: the old one until it is moved.RunInputsor check the SHA-256 values.Rejected: a generation folder with an atomic symbolic link (Windows without Developer Mode, Google Drive in Colab and zipped or copied output folders do not keep links); generation-named files with the manifest as pointer (changes the documented path or doubles the disk space).
On Windows the protocol is the same.
os.replacefails while another program has a file open: at the commit nothing has changed; after it, the run raisesPublicationIncompleteErrorand the next run finishes the moves. Folders are not fsynced on Windows, and the SIGKILL tests are POSIX-only.Resume. Design §1 says the staging folder is removed "at the start of the next run". It is kept until a run publishes, so a second interruption loses nothing; every run re-validates every staged row at its start (accepted by the owner and coordinator on that condition; question 69).
#90. A file's fingerprints must all agree: the two taken around its header read (the manifest entry records that fingerprint); the one taken just after its pixels are read (only then are its rows staged); the one taken for every analysed image just before the commit. Any difference is
source_changed_during_run(an error): nothing is published, the earlier generation is kept, and the other images' staged rows stay for the next run.Notes
RunInputsrefuses them)..<name>.<random>.tmpin the output folder (on main too).run/publish.py, resolved hunk by hunk); Micrometre conversions are always positive and finite; check and design exit 1 on errors (#93) #133 (plan §10/§12 only); same_objects is a one-to-one matching; size and plane-count warnings follow the confirmed roles (#67, #74) #131 (plan §12 only).Measured (fingerprint cost)
OME-Zarr store with 20,484 files, shared machine (load ~5, noisy): one fingerprint 186–203 ms on main vs 252–279 ms here; 3 walks per reused image and 4 per image read (1 on main); no-op
restart0.25–0.28 s vs 0.91–0.95 s; first-run difference below the noise; table byte-identical (5a6211b2…). No performance claim is made.Tests
Each fails on main:
test_inventory.py(#86 counterexample, links to folders),test_resume.py(SIGKILL while reading then resumed byte-identically; discards with reasons; #90 during the read, before the commit, after the header, an OME-Zarr chunk),test_publication.py(failures before/after the commit and at clean-up; SIGKILL before each of the 24 file-changing steps),test_convert_run.py/test_convert_integrity.py(the new error).Full gate on the final head (Linux, Python 3.13,
.[dev,notebook,zarr], as root):ruff checkpassed;ruff format --check201 files formatted;tools/check.py --rulespassed (baseline only shrank:test_run.py982 → 862; no# noqaadded);pytest1997 passed, 2 skipped (test_discovery.py:665, root ignores file modes;test_cli_log.py:165, from #128, needs a case-insensitive file system).https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Generated by Claude Code