Skip to content

Commit 105c4a9

Browse files
fix: Make schema registry building fully deterministic (#1220)
## Summary - Fixes non-deterministic schema resolution in `_get_registry()` that caused `nM` unit validation failures on some platforms (Linux CI) but not others (macOS) - `Path.rglob()` returns files in OS/filesystem-dependent order. When the flow-cytometry bundled units schema (771 defs, missing `nM`) was processed before the standalone (773 defs, has `nM`), the old first-wins logic for standalone registration prevented the correct schema from being used - Sorts `rglob` results for consistent iteration order across all platforms - Applies size comparison uniformly to both standalone and bundled schemas, with standalone winning on ties (21 tie cases exist across the schema set) - Verified fully deterministic: forward vs reverse iteration produces byte-identical registry contents ## Root Cause The standalone registration path used `if schema_id not in schemas_by_id` (first-wins, no size check), while the bundled path used `if bundled_size > sizes_by_id.get(...)` (largest-wins). When a bundled subset was encountered first, the standalone could never override it. ## Test plan - [x] All 734 parser tests pass - [x] All 379 discover_vendor + schema tests pass - [x] Determinism proven: building registry with sorted-forward vs sorted-reverse file lists produces identical results for all 105 schema `$id`s - [x] `nM` resolves correctly regardless of iteration order - [x] Lint passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce359b4 commit 105c4a9

1 file changed

Lines changed: 15 additions & 4 deletions

File tree

src/allotropy/allotrope/schemas.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,32 +273,43 @@ def _get_registry() -> Registry[dict[str, Any]]:
273273
under $defs with their own $id. When a bundled copy is LARGER than the standalone
274274
(more enum values, more definitions), we use the bundled version so that extended
275275
schemas take precedence. When a bundled copy is a subset, we keep the standalone.
276+
277+
Resolution is fully deterministic: files are sorted, the largest version of each $id
278+
wins, and on size ties standalone schemas take precedence over bundled copies.
276279
"""
277280
global _registry # noqa: PLW0603
278281
if _registry is not None:
279282
return _registry
280283

281284
schemas_by_id: dict[str, dict[str, Any]] = {}
282285
sizes_by_id: dict[str, int] = {}
286+
is_standalone: dict[str, bool] = {}
283287

284-
for schema_file in SCHEMA_DIR_PATH.rglob("*.schema.json"):
288+
for schema_file in sorted(SCHEMA_DIR_PATH.rglob("*.schema.json")):
285289
with open(schema_file, encoding=DEFAULT_ENCODING) as f:
286290
schema = json.load(f)
287291
schema_id = schema.get("$id")
288292
if not schema_id:
289293
continue
290-
if schema_id not in schemas_by_id:
294+
schema_size = _schema_content_size(schema)
295+
existing_size = sizes_by_id.get(schema_id, -1)
296+
if schema_size > existing_size or (
297+
schema_size == existing_size and not is_standalone.get(schema_id, False)
298+
):
291299
schemas_by_id[schema_id] = schema
292-
sizes_by_id[schema_id] = _schema_content_size(schema)
300+
sizes_by_id[schema_id] = schema_size
301+
is_standalone[schema_id] = True
293302

294303
for val in schema.get("$defs", {}).values():
295304
if not isinstance(val, dict) or "$id" not in val:
296305
continue
297306
bundled_id = val["$id"]
298307
bundled_size = _schema_content_size(val)
299-
if bundled_size > sizes_by_id.get(bundled_id, -1):
308+
existing_size = sizes_by_id.get(bundled_id, -1)
309+
if bundled_size > existing_size:
300310
schemas_by_id[bundled_id] = val
301311
sizes_by_id[bundled_id] = bundled_size
312+
is_standalone[bundled_id] = False
302313

303314
_registry = Registry().with_resources(
304315
(

0 commit comments

Comments
 (0)