diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a763d8..8670ff9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ are in [docs/plan.md](docs/plan.md) §12. Upgrading from v0.3: see the ## Unreleased +- **Re-pin the vendored Spec to b197ac1 (MorphoBricks-Spec PR 12, owner decision UR1).** The + reserved names of section 3.2 now also apply to `used_ids.channels`: an entry whose case-folded + form is on the list is `E_ID_RESERVED`, as for a declared channel ID (a listed ID reserves it). + The other three `used_ids` kinds are unchanged. New fixtures `i135` (invalid) and `v37` + (valid); the reference validator is updated. Every fixture is exact and the cross-check against + the reference validator has 0 differences. Before this change the reader accepted `i135`. - **The records append is rolled back with the description (K4 of external review batch 26).** `_append_records` appended straight to the live `morphobricks.records.jsonl`; a `write`, `flush` or `fsync` that failed after bytes were written left a torn line, or records naming a diff --git a/src/morphobricks_preprocessing/description_identity.py b/src/morphobricks_preprocessing/description_identity.py index 60403333..23156a53 100644 --- a/src/morphobricks_preprocessing/description_identity.py +++ b/src/morphobricks_preprocessing/description_identity.py @@ -157,14 +157,16 @@ def _masks(doc: Table, ids: Identities) -> None: def _used_ids(doc: Table, ids: Identities) -> None: """Entries follow the identity rules of their kind and share its collision scope. - The reserved list is not applied to ``used_ids`` entries: the text says "the identity rules - of their kind" and no fixture decides it (the reference validator does the same). + The reserved list of section 3.2 applies to ``used_ids.channels`` entries like a declared + channel ID (owner decision UR1, Spec 4.9): a listed ID reserves it. The registry has no list + for mask IDs, so the other three kinds are unchanged. """ for kind in USED_KINDS: for index, value in enumerate(doc["used_ids"][kind]): path = f"used_ids.{kind}[{index}]" if kind == "channels": ids.token(value, path) + ids.reserved(value, path) else: ids.label(value, path) ids.nfc(value, path) diff --git a/tests/spec_fixtures/PIN.toml b/tests/spec_fixtures/PIN.toml index b43b39a9..937f2c25 100644 --- a/tests/spec_fixtures/PIN.toml +++ b/tests/spec_fixtures/PIN.toml @@ -1,13 +1,13 @@ # The MorphoBricks-Spec commit these files are copied from (conformance plan section 1). # Updating the pin is a reviewed change: see README.md in this folder. repository = "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/CellMigrationLab/MorphoBricks-Spec" -commit = "7dbae0c600ac051b492f4d5fe41dd0b6008649db" +commit = "b197ac19d7a577cabe5281a81fa0fa59aef37146" # `git rev-parse :fixtures`, the git tree-object ID of the fixtures/ folder. The # vendored folder must hash to it, so one changed, added or missing byte fails a test. -fixtures_tree = "bb336c9494c371e0ba188f9be31832fdf19b0a14" +fixtures_tree = "5ac38b9fc91b0c461ee6585275e32b371029d4c0" # `git rev-parse :`, the git blob ID of each other vendored file. [files] -"docs/experiment-v1.md" = "73662c0b76a18250958beb885b016e4ced56d019" -"tools/audit/refval.py" = "0c8274429919108bf013e32e8dd0563e081c47b3" +"docs/experiment-v1.md" = "f162c5dc9ca395838785c23e999650391b86f81f" +"tools/audit/refval.py" = "de5a2cdb53ecb5bda7493c5b8cab2908e6e32fe6" diff --git a/tests/spec_fixtures/docs/experiment-v1.md b/tests/spec_fixtures/docs/experiment-v1.md index 73662c0b..f162c5dc 100644 --- a/tests/spec_fixtures/docs/experiment-v1.md +++ b/tests/spec_fixtures/docs/experiment-v1.md @@ -82,7 +82,8 @@ Time-lapses (T > 1) are allowed: the file has no T field, and the homogeneity of both is reported with both codes (§6, one code per broken rule). - **Reserved list** (one list, shared by channel IDs and mask IDs; AQ4). A channel ID or a mask ID whose case-folded form (`str.casefold()`) is in the list is `E_ID_RESERVED`; `ring`, `Ring` and - `RING` are all refused. The list is closed: a new entry needs a new `schema_version`. + `RING` are all refused. An entry of `used_ids.channels` is refused the same way, since a listed + ID reserves that ID (§4.9). The list is closed: a new entry needs a new `schema_version`. | Names | Source | |---|---| @@ -375,7 +376,7 @@ stays in the brick that owns it. files names a changed entity (some member folder, position or alias it held is removed or reassigned, or its `source_folder` is another folder). Removing or reassigning is accepted only with an explicit, recorded user confirmation (the same record as for a mistyped ID, below); the confirmation of a removal that - §10 requires is that record. An ID that was removed and comes back is `R_USED_ID_REUSED` + §10 requires is that record, and every removal is also written as a `removed` record (below). An ID that was removed and comes back is `R_USED_ID_REUSED` (§4.9). A condition or technical repeat ID is not its folder label (§4.6a): a folder that is renamed or replaced on disk under an existing ID is a removed folder plus a new pending one (§7), and giving the existing ID the new `source_folder` is a reassignment under (c). @@ -387,13 +388,38 @@ stays in the brick that owns it. records file is never rewritten or truncated, and nothing about a re-confirmation is stored in `morphobricks.toml`. Products keyed on that ID are invalidated. - **Encoding of the records file.** UTF-8, one JSON object per line, each line ending in LF, keys - in code point order, non-ASCII characters unescaped. Keys: `type` (`"reconfirmation"`), + in code point order, non-ASCII characters unescaped. The key `type` tells the record types + apart: `"reconfirmation"`, `"image_count_changed"` and `"removed"`. Each type has its own fixed + key set; a line of one type carries no key of another. The first type is the only one written + before this addition, and its lines are unchanged. Keys of `reconfirmation`: `type`, `id_kind` (`conditions`, `channels`, `repeats` or `technical_repeats`), `old_id`, `new_id` (`null` when the same ID lost or was reassigned something), `confirmed_on` (`YYYY-MM-DD`, the date of the user's action), `old_sha256` (of the published file replaced, or `absent`) and `new_sha256` (of the file written). The records are appended after the file is replaced. A records - file whose last line lacks its LF, or that holds a line that is not a JSON object, is refused - before anything is published. + file whose last line lacks its LF, that holds a line that is not a JSON object, or that holds an + object whose `type` is none of the three, is refused before anything is published. +- **Record of a removal (`removed`, owner decision RR2).** Written for **every** removal of an + imaging unit, technical repeat, repeat or condition that the user confirms (§10), including a + removal whose ID is held nowhere else afterwards; it is written in addition to a + `reconfirmation` where §4.6 requires one. Keys: `type`, `entity` (`unit`, `technical_repeat`, + `repeat` or `condition`), `condition_id`, `condition_source_folder`, `repeat_id`, + `source_repeat`, `technical_repeat_id`, `technical_repeat_source_folder`, and `confirmed_on` + (`YYYY-MM-DD`, the date of the user's confirmation; a `removed` line is itself that + confirmation). A key that the entity does not name is `null`: a `unit` (one technical repeat + entry of one member of one repeat) names all six; a `technical_repeat` names its two keys; a + `repeat` its `repeat_id`; a `condition` its two condition keys. The source folder labels are the + exact labels of the removed published file. A removed member folder is written as one `unit` + record per imaging unit it held, and an entity that held units is followed by the `unit` + records of those units, so every removed folder label appears in a record. Removing an entity + never removes its ID from `used_ids` (§4.9). +- **Record of an image-count change (`image_count_changed`, owner decision RR1).** Written when + the `image_count` of a confirmed member changes and nothing else about the published file does + (§10); it needs no confirmation and reopens nothing. Keys: `type`, `condition_id`, + `condition_source_folder`, `repeat_id`, `source_repeat`, `technical_repeats` (the member's + imaging units as an array of objects `{"id", "source_folder"}`, sorted by `id` in code point + order), `old_image_count`, `new_image_count` (integers, each at least 1) and `recorded_on` + (`YYYY-MM-DD`, the date the change was recorded at publication; it is not a confirmation and is + never copied into `confirmed_on`). - **A case-only correction needs a new ID (AQ2).** `ACTIN` cannot be corrected to `Actin`: the retired spelling stays in `used_ids` and the new one collides with it (`E_ID_CASE_COLLISION`, §4.9). The correction is a new, non-colliding ID (for example @@ -506,6 +532,11 @@ date or a reason. repeats, Token for channels; NFC, §3.3), have no duplicates (`E_DUPLICATE_VALUE`), and share the case-collision scope of the kind's declared IDs (`E_ID_CASE_COLLISION`): a retired `ctrl` cannot come back as `CTRL`. At least one entry per kind (`E_EMPTY`). +- **Reserved names in `used_ids.channels`** (owner decision UR1, 7 November 2026). A channel entry + follows the reserved list of §3.2 like a declared channel ID: an entry whose case-folded form is + in the list is `E_ID_RESERVED`. A listed ID reserves that ID, so a name no channel may bear + cannot be reserved by being listed. The registry has no list for mask IDs (it holds the four + kinds above), so the list applies to channels only; the other three kinds are unchanged. - Source folder labels are not IDs and are never listed. Every declared ID of a kind MUST be listed (`E_USED_ID_MISSING`). A listed ID that is not declared is a **retired** ID: its entity was removed. @@ -568,7 +599,7 @@ for NFC only (phase 4) before it is resolved (phase 5). | 3 | `E_REPEAT_UNCONFIRMED` | `confirmed = false` in a published file | | 4 Identity | `E_ID_TEXT` | Label rule violated | | 4 | `E_ID_SYNTAX` | Token regular expression violated | -| 4 | `E_ID_RESERVED` | Name on the reserved list of §3.2 (channel and mask IDs), or a mask ID with an `in` part | +| 4 | `E_ID_RESERVED` | Name on the reserved list of §3.2 (channel and mask IDs, and entries of `used_ids.channels`, §4.9), or a mask ID with an `in` part | | 4 | `E_ID_NOT_NFC` | An identity (key, reference, source repeat, `source_folder`, unit `channels` or `source_channels` entry, `used_ids` entry) is not Unicode NFC; never normalized | | 4 | `E_ID_CASE_COLLISION` | Two identities of one scope equal ignoring case (`used_ids` entries share the scope of their kind; source folders have their own scopes, §3.3) | | 4 | `E_SINGLE_MISUSE` | `single` in another case or not alone, or a technical repeat ID or condition ID `single` whose `source_folder` is not `single` (or the reverse) | @@ -749,6 +780,8 @@ silently; edits go to a separate draft and are published atomically after valida channels, layout or grouping change: a new or removed unit or member, a unit's ordered channel list, or the pairing of folders into repeats. New FOVs (or removed ones) inside an existing unit whose confirmed layout still holds do **not** reopen it; they are reported and recorded. A + changed `image_count` of a confirmed member, when nothing else changes, is recorded as an + `image_count_changed` record (§4.6). A changed `image_count` alone is therefore not a reopening: a new `confirmed_on` beside only a changed `image_count` is `R_CONFIRMED_ON_CHANGED` (§4.4a). - **Adding a member is allowed (AQ1).** A new member folder or unit joins an existing repeat or @@ -756,7 +789,9 @@ silently; edits go to a separate draft and are published atomically after valida confirmation of that repeat, and it never changes an existing ID. Only removing or reassigning existing members counts as a changed entity. - A removed member is reported and removed only on user confirmation, which is the recorded - confirmation of §4.6 (without it the candidate is `R_CONFIRMED_ID_CHANGED`); a repeat left + confirmation of §4.6 (without it the candidate is `R_CONFIRMED_ID_CHANGED`); every confirmed + removal of a unit, technical repeat, repeat or condition, whether or not an ID survives it, is + also written as a `removed` record (§4.6); a repeat left without members is removed from the file, and its ID stays in `used_ids` and is never reused (`R_USED_ID_REUSED`, `R_USED_ID_DROPPED`; §4.9). - A new folder whose label equals an existing repeat ID is proposed for that repeat but stays diff --git a/tests/spec_fixtures/fixtures/README.md b/tests/spec_fixtures/fixtures/README.md index de26ffc7..6430fa8f 100644 --- a/tests/spec_fixtures/fixtures/README.md +++ b/tests/spec_fixtures/fixtures/README.md @@ -16,7 +16,7 @@ is executed by this repository. `R_HETEROGENEOUS_LAYOUT_NOT_ENABLED` (lifted at Stage 3). - Physical cases that need images (within-unit divergence, header contradictions, mask pixel contradictions, reconciliation) cannot be expressed by a TOML file alone; they are listed as - scenarios P01-P33 in the [conformance plan](../docs/conformance-plan.md#3-physical-scenarios-preprocessing-owned), + scenarios P01-P35 in the [conformance plan](../docs/conformance-plan.md#3-physical-scenarios-preprocessing-owned), built on the valid files below. - `v18` MUST parse to the same model as `v01` (round trip); `v21` differs from `v01` only in `display_name`, `target` and `treatment_key` values and `v16` only in `display_name` values, so @@ -65,9 +65,9 @@ The matrix audit ([`docs/audits/matrix-audit-2026-11-07.md`](../docs/audits/matr adds `v35`, `v36` and `i128`-`i130` for structures the catalogue lacked (a flat experiment, a biological-repeat folder named like a forbidden key, `single` rules for condition folders and members). The owner decisions of 7 November 2026 (ADR 0001, SQ6) add `i131`-`i133` (the -`single` rules for conditions) and `i134` (a duplicate key inside an inline table). Its audit-only validator, [`tools/audit/refval.py`](../tools/audit/refval.py) -(`python -I tools/audit/refval.py fixtures .`), accepts 36 of 36 valid files with exactly the -listed warnings, refuses 134 of 134 invalid files with exactly the listed code set from the listed +`single` rules for conditions) and `i134` (a duplicate key inside an inline table); the owner decision of 7 November 2026 on reserved names in `used_ids` (UR1) adds `v37` and `i135`. Its audit-only validator, [`tools/audit/refval.py`](../tools/audit/refval.py) +(`python -I tools/audit/refval.py fixtures .`), accepts 37 of 37 valid files with exactly the +listed warnings, refuses 135 of 135 invalid files with exactly the listed code set from the listed phase, gives the listed `R_*` set for 12 of 12 transitions, and round-trips every valid and transition file through the canonical writer. It is an audit tool, not a reader: the conformance plan's single reference reader stays preprocessing. @@ -157,7 +157,7 @@ This is a check of the fixtures against the text, not a conformance claim for an | Adding is not a change; removing or reassigning is | `t03`, `t06`, `t07`, `t08`; P07, P15 | | Case-only correction needs a new ID; set of publication codes | `t02`, `t09`, `i95`; P16 | | RGB images (declared type, fixed tokens, no aliases) | `v28`, `v29`, `i100`-`i106`, `i119`; P13, P23, P24 | -| Reserved names (one list for channel and mask IDs) | `i59`, `i61`, `i107`-`i109`, `v32` | +| Reserved names (one list for channel and mask IDs; also for `used_ids.channels`) | `i59`, `i61`, `i107`-`i109`, `i135`, `v32`, `v37` | | Forbidden names apply to ID keys | `i113`-`i115` | | Folder labels never restricted: condition and technical-repeat IDs separate from `source_folder` | `v33`, `v34`, `i113`, `i115`, `i120`-`i127`, `t10`-`t12`; P32, P33 | | One folder, one ID; `single` iff no folder level (technical repeats: `i125`; conditions: `i131`-`i133`) | `i122`-`i125`, `i131`-`i133` | @@ -203,6 +203,7 @@ This is a check of the fixtures against the text, not a conformance claim for an | AQ2 | Case-only ID correction needs a new ID; publication reports every `R_*` code | `t02`, `t09`, `i95`; P16 | | AQ3 | `target` descriptive; MorphoCoverage may only prefill, confirmed inside MorphoCoverage | `v27`; P31 | | AQ4 | One reserved list: compartments plus MorphoRelate reserved column names | `i59`, `i61`, `i107`-`i109`, `v32` | +| UR1 | The reserved list also applies to `used_ids.channels` entries (they reserve those IDs) | `i135`, `v37` | | AQ5 | Canonical repeat ID added as `repeat_group`; the FOV key keeps the source label | `v03`, `v30`; P25 | | AQ6 | RGB accepted as a declared type (`image_type = "rgb"`, tokens `R`, `G`, `B`); supersedes D8 | `v28`, `v29`, `i100`-`i106`, `i119`; P13, P23, P24 | | AQ7 | No trimming: header names exact after NFC of both sides | `v31`, `i110`; P26 | @@ -214,8 +215,10 @@ This is a check of the fixtures against the text, not a conformance claim for an | AQ13 | Forbidden names apply to ID keys; NFC of channel references; one code per broken rule | `i113`-`i118` | | FR1 | Folder labels are never restricted by the reserved or forbidden names: conditions and technical repeats have a canonical ID separate from the required `source_folder`; one folder, one ID; a folder change for an existing ID is a reassignment; the derived CSV keeps the folder labels | `v33`, `v34`, `i113`, `i115`, `i120`-`i127`, `t10`-`t12`; P32, P33 | | FR2 | Derived CSV byte-identity is guaranteed only for repeat groups with a single `confirmed_on`; a legacy group with several dates gets the latest, the migration note keeps the per-row dates | `v13`; P30 | +| RR1 | Record type `image_count_changed` (a confirmed unit's image count changes and nothing else does; nothing reopens) | P34 | +| RR2 | Record type `removed` for every confirmed removal of a unit, technical repeat, repeat or condition | P35 | -## Valid fixtures (36) +## Valid fixtures (37) | Fixture | Source | Expected | Homogeneous | Case | |---|---|---|---|---| @@ -255,6 +258,7 @@ This is a check of the fixtures against the text, not a conformance claim for an | [`v34_technical_repeat_folder_on_forbidden_list`](valid/v34_technical_repeat_folder_on_forbidden_list.toml) | Owner decision FR1 | pass | yes | Repeat rep1 has two technical-repeat folders, `files` and `overrides`, both forbidden names (experiment-v1 4.7). They are the `source_folder` values; the confirmed IDs are `acq1` and `acq2`, and the same ID carries the same folder in both members. rep2 has no technical-repeat folder level (`single`). Homogeneous. | | [`v35_flat_experiment`](valid/v35_flat_experiment.toml) | Matrix audit 2026-11-07, case F01 | pass | yes | The experiment folder is itself the leaf: condition, repeat and technical repeat are all `single`. The condition `single` has `source_folder = "single"` ("no condition folder level", 4.2, 3.2) and is the only condition (owner decision SQ6, matrix audit MS-1). | | [`v36_repeat_folder_forbidden_name`](valid/v36_repeat_folder_forbidden_name.toml) | Matrix audit 2026-11-07, case F16 | pass | yes | Biological-repeat folders `z` paired into repeat `R1`; `source_repeat` is never restricted. The folder label `z` is not an allowed repeat ID (`i130`), so no ID is proposed and the user names `R1` (4.6a; owner decision SQ6, matrix audit MS-2). | +| [`v37_used_ids_reserved_near_misses`](valid/v37_used_ids_reserved_near_misses.toml) | Owner decision UR1 (Spec issue 11) | pass | yes | `v32` with the retired channel IDs `Ringer` and `label2` listed in `used_ids.channels`: names next to the reserved list are valid there too (experiment-v1 4.9). | ## Expected outputs (3) @@ -287,7 +291,7 @@ Valid alone; checked against the named published predecessor (refused with the l | [`t12_condition_folder_reassigned`](transitions/t12_condition_folder_reassigned.toml) | `valid/v33` | R_CONFIRMED_ID_CHANGED (against predecessor valid/v33, without a recorded confirmation) | The ID `ctrl` held the folder `control` in v33 and now holds the folder `ctl` (a folder renamed on disk and attached to the same ID without a recorded confirmation). A folder change for an existing ID is a reassignment, so a changed entity (experiment-v1 4.6, 7). Valid alone. | | [`t13_confirmed_on_with_image_count_only`](transitions/t13_confirmed_on_with_image_count_only.toml) | `valid/v01` | `R_CONFIRMED_ON_CHANGED` (N35 not reopened) | Against v01 only CTRL/rep1 `image_count` (12 to 17, new FOVs) and rep1 `confirmed_on` (2026-11-07) change: new FOVs do not reopen the confirmation (experiment-v1 10), so the new date is refused. | -## Invalid fixtures (134) +## Invalid fixtures (135) | Fixture | Source | Expected code(s) | Phase | Case | |---|---|---|---|---| @@ -425,3 +429,4 @@ Valid alone; checked against the named published predecessor (refused with the l | [`i132_condition_single_folder_other_id`](invalid/i132_condition_single_folder_other_id.toml) | Owner decision SQ6, matrix audit MS-1 | `E_SINGLE_MISUSE` | 4 | The condition `flat` has `source_folder = "single"`: a condition ID is `single` if and only if its folder is `single` (experiment-v1 3.2). | | [`i133_condition_single_not_alone`](invalid/i133_condition_single_not_alone.toml) | Owner decision SQ6, matrix audit MS-1 | `E_SINGLE_MISUSE` | 4 | The condition `single` (no condition folder level) next to the condition DrugA: it must be the only condition (experiment-v1 3.2). | | [`i134_duplicate_key_inline_table`](invalid/i134_duplicate_key_inline_table.toml) | Stage 1 design PR 0, experiment-v1 2.2 | `E_DUPLICATE_KEY` | 1 | `display_name` written twice inside the inline table of the channel DNA: a key declared twice in any spelling, inline tables included, is `E_DUPLICATE_KEY`, not `E_TOML_SYNTAX` (experiment-v1 2.2). | +| [`i135_used_id_channel_reserved`](invalid/i135_used_id_channel_reserved.toml) | Owner decision UR1 (Spec issue 11) | `E_ID_RESERVED` | 4 | The retired channel ID `Nucleus` in `used_ids.channels`: a listed ID reserves the ID, so it follows the reserved list of 3.2 (case-folded) like a declared channel ID (experiment-v1 4.9). | diff --git a/tests/spec_fixtures/fixtures/invalid/i135_used_id_channel_reserved.toml b/tests/spec_fixtures/fixtures/invalid/i135_used_id_channel_reserved.toml new file mode 100644 index 00000000..a2640d4f --- /dev/null +++ b/tests/spec_fixtures/fixtures/invalid/i135_used_id_channel_reserved.toml @@ -0,0 +1,71 @@ +# Fixture i135_used_id_channel_reserved +# Expected: E_ID_RESERVED +# Source: Owner decision of 7 Nov 2026 (MorphoBricks-Spec issue 11) +# The retired channel ID "Nucleus" in used_ids.channels is on the reserved list (experiment-v1 3.2, 4.9): a listed ID reserves the ID, so it follows the same list as a declared channel ID, case-folded. +schema = "morphobricks-experiment" +schema_version = 1 + +[conditions.CTRL] +source_folder = "CTRL" +display_name = "Control" + +[conditions.DrugA] +source_folder = "DrugA" +display_name = "Drug A" +reference = "CTRL" +target = "SRC" +treatment_key = "saracatinib" + +[channels.DNA] +display_name = "DAPI" +header_names = ["DAPI"] + +[channels.ACTIN] +display_name = "F-actin" +header_names = ["Phalloidin-488"] + +[repeats.rep1] +confirmed = true +confirmed_on = "2026-11-06" + +[repeats.rep1.members.CTRL] +source_repeat = "rep1" +image_count = 12 + +[repeats.rep1.members.CTRL.technical_repeats.single] +source_folder = "single" +channels = ["DNA", "ACTIN"] + +[repeats.rep1.members.DrugA] +source_repeat = "rep1" +image_count = 11 + +[repeats.rep1.members.DrugA.technical_repeats.single] +source_folder = "single" +channels = ["DNA", "ACTIN"] + +[repeats.rep2] +confirmed = true +confirmed_on = "2026-11-06" + +[repeats.rep2.members.CTRL] +source_repeat = "rep2" +image_count = 10 + +[repeats.rep2.members.CTRL.technical_repeats.single] +source_folder = "single" +channels = ["DNA", "ACTIN"] + +[repeats.rep2.members.DrugA] +source_repeat = "rep2" +image_count = 10 + +[repeats.rep2.members.DrugA.technical_repeats.single] +source_folder = "single" +channels = ["DNA", "ACTIN"] + +[used_ids] +conditions = ["CTRL", "DrugA"] +channels = ["ACTIN", "DNA", "Nucleus"] +repeats = ["rep1", "rep2"] +technical_repeats = ["single"] diff --git a/tests/spec_fixtures/fixtures/valid/v37_used_ids_reserved_near_misses.toml b/tests/spec_fixtures/fixtures/valid/v37_used_ids_reserved_near_misses.toml new file mode 100644 index 00000000..19c8a7f5 --- /dev/null +++ b/tests/spec_fixtures/fixtures/valid/v37_used_ids_reserved_near_misses.toml @@ -0,0 +1,43 @@ +# Fixture v37_used_ids_reserved_near_misses +# Expected: PASS +# Source: Owner decision of 7 Nov 2026 (MorphoBricks-Spec issue 11) +# Retired channel IDs next to the reserved list are valid in used_ids.channels (experiment-v1 4.9): Ringer and label2 are listed but not declared. Only the exact reserved names, compared case-folded, are refused there as for declared IDs (i135). +schema = "morphobricks-experiment" +schema_version = 1 + +[conditions.CTRL] +source_folder = "CTRL" + +[channels.c1] + +[channels.t0] + +[channels.z2] + +[channels.label] + +[repeats.rep1] +confirmed = true +confirmed_on = "2026-11-06" + +[repeats.rep1.members.CTRL] +source_repeat = "rep1" +image_count = 4 + +[repeats.rep1.members.CTRL.technical_repeats.single] +source_folder = "single" +channels = ["c1", "t0", "z2", "label"] + +[masks.Patches] +source_type = "patches" +representation = "instances" + +[masks.Labels] +source_type = "labels" +representation = "binary" + +[used_ids] +conditions = ["CTRL"] +channels = ["Ringer", "c1", "label", "label2", "t0", "z2"] +repeats = ["rep1"] +technical_repeats = ["single"] diff --git a/tests/spec_fixtures/tools/audit/refval.py b/tests/spec_fixtures/tools/audit/refval.py index 0c827442..de5a2cdb 100644 --- a/tests/spec_fixtures/tools/audit/refval.py +++ b/tests/spec_fixtures/tools/audit/refval.py @@ -398,10 +398,12 @@ def label_id(v, path, scope): for i, v in enumerate(doc["used_ids"][kind]): p = f"used_ids.{kind}[{i}]" if kind == "channels": - # AMBIGUITY: "the identity rules of their kind" -- Label + Token + NFC applied; - # the reserved list is not applied to used_ids entries (no fixture decides it). + # Owner decision of 7 Nov 2026 (Spec issue 11): a listed channel ID reserves it, + # so the reserved list of 3.2 applies as to a declared channel ID (4.9). _token(v, p, r) _nfc(v, p, r) + if v.casefold() in RESERVED: + r.e("E_ID_RESERVED", p, v) else: _label(v, p, r) _nfc(v, p, r) diff --git a/tests/test_spec_reader.py b/tests/test_spec_reader.py index 0a08922a..76c13ecb 100644 --- a/tests/test_spec_reader.py +++ b/tests/test_spec_reader.py @@ -24,7 +24,7 @@ def _name(item): def test_the_fixture_counts_are_the_catalogue_s(): - assert (len(VALID), len(INVALID), len(TRANSITIONS)) == (36, 134, 13) + assert (len(VALID), len(INVALID), len(TRANSITIONS)) == (37, 135, 13) @pytest.mark.parametrize("item", VALID, ids=_name)