Source code for exlab_wizard.sync.pre_sync_gate

"""Pre-Sync Gate. Backend Spec §7.3.

The Pre-Sync Gate is the contract by which validator findings prevent
the NAS sync queue from accepting a flagged run. A run is eligible iff:

- the validator engine reports zero hard-tier findings, OR
- every hard-tier finding has a matching active override entry in
  ``creation.json``'s ``validation_overrides`` array.

This module is the pure evaluator. The :class:`NASSyncClient` calls it
before inserting a queue row and, when the gate blocks, mutates the
``creation.json`` ``sync_status`` field to ``"blocked_by_validation"``.
"""

from __future__ import annotations

from pathlib import Path

from exlab_wizard.api.schemas import CreationJson
from exlab_wizard.cache.creation_writer import select_active_overrides
from exlab_wizard.constants import Tier
from exlab_wizard.validator.engine import (
    CreationValidationInput,
    Validator,
    _split_path_segments,
)
from exlab_wizard.validator.findings import Finding

__all__ = ["is_eligible"]


def _file_names_in_run(run_path: Path) -> list[str]:
    """Return the bare file names directly under ``run_path``.

    The Pre-Sync Gate is a creation-time-style validator pass against the
    on-disk run, so we re-collect file names rather than rely on the
    template-render output. The walk is shallow on purpose: §8.1 rules
    that scan content are not part of the gate's hard-tier set, so we
    only need names + the path segments.
    """
    if not run_path.exists() or not run_path.is_dir():
        return []
    return [entry.name for entry in run_path.iterdir() if entry.is_file()]


[docs] def is_eligible( *, validator: Validator, creation_json_path: Path, creation: CreationJson, ) -> tuple[bool, list[Finding]]: """Evaluate the §7.3 eligibility rule for a run. Returns ``(True, [])`` iff there is no hard-tier finding without an active override. Otherwise returns ``(False, blocking_findings)`` where ``blocking_findings`` is the list of unmasked hard-tier findings (so the caller can surface them in logs). The ``creation_json_path`` is the path to ``.exlab-wizard/creation.json``; the run directory is its parent's parent. The validator runs in creation-time mode (no walk; just rules over path segments + file names + the cached creation payload). """ run_path = creation_json_path.parent.parent proposed_path = creation.paths.local or str(run_path) file_names = tuple(_file_names_in_run(run_path)) params = CreationValidationInput( proposed_path=proposed_path, variables=creation.variables, file_names=file_names, run_kind=creation.run_kind, ) findings = validator.validate_creation(params) # Active override classes from the run's validation_overrides list. active_classes: set[str] = { e["problem_class"] for e in select_active_overrides(creation.validation_overrides) if "problem_class" in e } blocking: list[Finding] = [ f for f in findings if f.tier == Tier.HARD.value and f.rule not in active_classes ] # The mode-prefix mismatch rule needs a parent-segment context that # isn't reachable from creation.paths.local alone if the LIMS short # ID component is missing; for a more complete gate we also pass # the resolved on-disk path. We re-run the splitter here to make # sure the run is evaluated against its actual segments. _ = _split_path_segments # reference to keep the symbol re-exported if blocking: return False, blocking return True, []