Source code for exlab_wizard.validator.findings

"""Validator finding dataclass and serialization helpers.

Backend Spec §11.8 (finding shape contract) and §8.1 (rule catalog).

Every validator finding (creation-time or audit mode) is materialised as
a frozen :class:`Finding` instance. Frozen so findings can be put in
sets / sorted / hashed for the delta computation in the audit pub-sub
channel (Backend Spec §11.8 -- the 30-second background refresh diffs
two snapshots and emits adds/removes; that diff requires hashable
elements).

The on-the-wire JSON shape lives in §11.8 (verbatim copy below)::

    {
      "rule": "unresolved_placeholder_token",
      "tier": "hard",
      "run_path": "/data/lab/CONFOCAL_01/PROJ-0042/Run_<run_date>",
      "offending_path": "/data/lab/CONFOCAL_01/PROJ-0042/Run_<run_date>",
      "offending_kind": "directory_segment",
      "matched_token": "<run_date>",
      "rule_detail": "Angle-bracket identifier token <run_date>...",
      "synced_under_prior_policy": false,
      "override_active": false
    }

:meth:`Finding.to_dict` and :meth:`Finding.from_dict` round-trip this
shape; both modes (creation-time and audit) produce dictionaries that
are byte-identical given byte-identical inputs (the §11.8 determinism
contract).
"""

from __future__ import annotations

from dataclasses import asdict, dataclass
from typing import Any

__all__ = ["Finding"]


[docs] @dataclass(frozen=True, slots=True) class Finding: """One validator finding. Backend Spec §8.1, §11.8. Frozen so findings can be put in sets / sorted / hashed for the delta computation in the audit pub-sub channel (§11.8). ``slots`` is set so that the dataclass does not allocate a per-instance ``__dict__`` -- audit mode can produce thousands of findings on a large tree and the per-instance overhead matters. Field semantics (§11.8): - ``rule`` -- the §8.1 rule that fired. One of the :class:`~exlab_wizard.constants.ProblemClass` values; kept as a plain string here so the dataclass does not depend on the closed enum at the typing surface (the enum is the canonical truth, but a string field keeps round-tripping via ``msgspec``/JSON cheap). - ``tier`` -- ``"hard"`` or ``"soft"`` per the §8.1.6 tier mapping. - ``run_path`` -- the run-level directory ancestor (or project/equipment level for orphans at those levels). - ``offending_path`` -- the absolute path of the artefact that tripped the rule. May equal ``run_path`` when the rule applies to the run-level directory itself. - ``offending_kind`` -- one of ``directory_segment``, ``file_name``, ``file_content`` (§11.8). - ``matched_token`` -- the substring (e.g. ``"<run_date>"``) or reserved name (e.g. ``"CON"``) that triggered the rule. ``None`` for rules that don't have a single matched token (e.g. orphan). - ``rule_detail`` -- a short human-readable description suitable for the Problems-tab row. Defaults to ``""`` so the field is always present in the JSON shape. - ``synced_under_prior_policy`` -- set to ``True`` when audit mode finds a hard-tier finding on a run whose ``creation.json`` ``sync_status`` is already ``"synced"`` (Backend Spec §7.3). Defaults to ``False`` for creation-time findings. - ``override_active`` -- set to ``True`` when the run's ``validation_overrides`` contains a non-revoked entry whose ``problem_class`` matches ``rule``. Defaults to ``False``. """ rule: str tier: str run_path: str offending_path: str offending_kind: str matched_token: str | None = None rule_detail: str = "" synced_under_prior_policy: bool = False override_active: bool = False
[docs] def to_dict(self) -> dict[str, Any]: """Return the §11.8 JSON shape for this finding. The dictionary is suitable for ``msgspec.json.encode`` / ``json.dumps``. Field order matches the §11.8 example so the output is reproducible byte-for-byte across hosts (the determinism contract). """ return asdict(self)
[docs] @classmethod def from_dict(cls, payload: dict[str, Any]) -> Finding: """Reconstruct a :class:`Finding` from its §11.8 JSON shape. The inverse of :meth:`to_dict`. Unknown fields are ignored so that a future minor schema bump does not break older readers (per the §11.9 reader policy spirit, applied to in-flight finding payloads). """ return cls( rule=payload["rule"], tier=payload["tier"], run_path=payload["run_path"], offending_path=payload["offending_path"], offending_kind=payload["offending_kind"], matched_token=payload.get("matched_token"), rule_detail=payload.get("rule_detail", ""), synced_under_prior_policy=payload.get( "synced_under_prior_policy", False, ), override_active=payload.get("override_active", False), )