Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ are in [docs/plan.md](docs/plan.md) §12. Upgrading from v0.3: see the

## Unreleased

- **Contract: the final mask check is as late as practicable, not a lock (#222, docs only).** Source masks are checked right before the manifest replace and ordinary concurrent edits are detected; the files are not locked, so a change between that check and the replace is not detected, and the manifest binds the bytes as they were at the final check (owner decision of 8 November 2026: documented, not snapshotted). `docs/contract.md` §9.4.
- **Notebook Step 1 cards: names, IDs, pairing and references (Stage 1 PR 14, draft).** After the
labels are confirmed, *Names for your experiment* proposes the folder names as IDs (only where
valid as they are), takes an optional shown-as name, and adopts the IDs only when the user clicks
Expand Down
2 changes: 1 addition & 1 deletion docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ raise `ValueError`. Tested for the Spec fixtures `v03 v08 v13 v28 v30 v33 v34`.
|---|---|
| Staging | As soon as a run has read one image, that image's rows are staged in the output folder's `.staging` folder: a `<id>.csv` with its rows (the normalization brick's writer, the table's format) and then `<id>.json` (staging version 1) naming the image, its staging key and the rows file's SHA-256. The staging key is the image's row key (§9: fingerprint, labels, axes, roles, `singleton_roles`, `axes_from`, read path, bins and histogram settings) with the pixel type, the table's schema version and the versions of `morphobricks-preprocessing` and `morphobricks-normalization`. A rows file without its record (killed between the two) was never staged. |
| Resume | A run killed part-way (a crash, a kill, a lost Colab session) is resumed by the next run: each image that needs new rows whose staged key is the key it would have now (and whose fingerprint is still that; with content verification, whose content SHA-256, part of the key, too) takes its staged rows without its pixels being read (`RunResult.resumed`, report `resumed`). Any other staged rows are discarded and reported with the reason (report `staged_discarded`): the image changed since its rows were staged; its roles, labels, folder or row settings changed; the experiment's pixel type changed; another version staged them; they could not be read (reported under the image the record names, else the record's file name); the table already holds its rows; the image is no longer analysed; a `full` run (which reuses no staged row). Nothing is reused when identity is uncertain. A run that stops keeps `.staging`, so the next run resumes; a run that publishes removes it after the commit. Every check still runs over the whole experiment (design v0.4 §1). A resumed table is byte-identical to an uninterrupted run's. |
| Source changes during a run (issue #90) | Each image's header is read between two fingerprints that must agree (`preflight(bind_headers=True)`), and that fingerprint is the one its manifest entry records; the image must still have it just after its pixels are read (only then are its rows staged); and every analysed image (reused, resumed or read) is fingerprinted again just before the commit (in a `verify_content` run also hashed again, as the last step before the manifest's rename, above). An image that changed or went missing at any of these points is the error `source_changed_during_run` naming it: no scientific outputs are published and sources are untouched; the discovery report is written; the earlier generation is kept, and the rows staged so far stay staged (the changed image's own rows are not staged, or are discarded by the next run). Masks are bound the same way (issue #160): each mask's header is read between two fingerprints that must agree (`find_masks(bind=True)`, `MaskFile.bound`; else `mask_changed_during_check`), that fingerprint is the one its manifest entry records (before #160 the fingerprint was taken when the manifest was written), and every mask must still have it right before the commit; a mask that changed or went missing is the error `mask_changed_during_run` naming it, with what changed, and nothing is published (`mask_binding`). |
| Source changes during a run (issue #90) | Each image's header is read between two fingerprints that must agree (`preflight(bind_headers=True)`), and that fingerprint is the one its manifest entry records; the image must still have it just after its pixels are read (only then are its rows staged); and every analysed image (reused, resumed or read) is fingerprinted again just before the commit (in a `verify_content` run also hashed again, as the last step before the manifest's rename, above). An image that changed or went missing at any of these points is the error `source_changed_during_run` naming it: no scientific outputs are published and sources are untouched; the discovery report is written; the earlier generation is kept, and the rows staged so far stay staged (the changed image's own rows are not staged, or are discarded by the next run). Masks are bound the same way (issue #160): each mask's header is read between two fingerprints that must agree (`find_masks(bind=True)`, `MaskFile.bound`; else `mask_changed_during_check`), that fingerprint is the one its manifest entry records (before #160 the fingerprint was taken when the manifest was written), and every mask must still have it right before the commit; a mask that changed or went missing is the error `mask_changed_during_run` naming it, with what changed, and nothing is published (`mask_binding`). Source masks are checked as late as practicable (right before the manifest's `os.replace`, in `before_commit`), and ordinary concurrent edits are detected. The source files are not locked and not snapshotted (owner decision of 8 November 2026, issue #222), so a change in the instant between that final check and the manifest replace is not detected: the manifest binds the masks' bytes as they were at that final check. |
| Generation | The run's published files: `plane_histograms.csv`, `experiment_design.csv` (with a design), `run_manifest.json` and the report files, and the files the generation removes (`mask_relationships.json`, §9.2). |
| Publication | (1) The whole new generation is written into `.publish` in the output folder and flushed to disk, then `.publish/publication.json` (publication version 1: each file's SHA-256 and the files removed). (2) **Commit, the one atomic step:** `os.replace` of the new `run_manifest.json` over the old one, then the output folder is flushed (`fsync` of the folder, POSIX), so the commit is on the disk when it returns and a power cut after it keeps the new generation. Nothing in the output folder changes before it. (3) The other files are moved into place (`os.replace` each), the removed files removed, `.publish` deleted, then `.staging`. |
| Interrupted publication | Whatever next takes the output folder's lock (`run`, `check`, `masks`, `design`, `calibrate`, the notebook's writes) first finishes a committed publication or deletes an uncommitted `.publish`, record first (the earlier generation is then untouched) (`run.generation.finish_publication`). **Committed** means the staged manifest was moved: the record is there, `.publish/run_manifest.json` is not, and the manifest in place is the record's; the remaining moves and removals are then done. Anything else is not committed, also when the manifest in place is byte-identical to the staged one (an identical rerun refused before its commit); a `.publish` whose record cannot be removed is left whole (its staged manifest keeps it uncommitted) for the next holder of the lock. A failure to move a file after the commit (for example a table held open on Windows) raises `PublicationIncompleteError`: the new generation is current, and the next holder of the lock finishes the moves. |
Expand Down
Loading