Source code for pain001.corpus.rules.ladder

# Copyright (C) 2023-2026 Pain001. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Licensed under either of the Apache License, Version 2.0 or the MIT
# License, at your option. You may not use this file except in
# compliance with one of those licences. Copies are provided in
# LICENSE-APACHE and LICENSE-MIT.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licences is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the applicable Licence for the specific language
# governing permissions and limitations.

"""The validation ladder for one built corpus file (ADR-0003, L0 to L3).

* **L0** the edition's XSD (the builder already refuses a failure);
* **L1** the MDR cross-element rules;
* **L2** the scheme and rail profiles the scenario lists, run on the
  file's row projection;
* **L3** the overlays that apply to the scenario or its family.

:func:`run_ladder` returns a JSON-ready record the build writes into the
file's provenance sidecar and the gate re-checks; :func:`ladder_passes`
says whether any rung produced an error-severity finding.
"""

from __future__ import annotations

from typing import Any

from pain001.corpus.registry import Scenario
from pain001.corpus.rules.mdr import evaluate_mdr
from pain001.corpus.rules.overlays import Overlay, evaluate, load_overlays
from pain001.corpus.rules.projection import rows_from_xml
from pain001.templates import DEFAULT_TEMPLATE_REGISTRY
from pain001.validation.schemes import validate_scheme
from pain001.xml.validate_via_xsd import collect_xsd_validation_errors


[docs] def applicable_overlays( scenario: Scenario, pool: list[Overlay], variant: str | None = None, version: str | None = None, ) -> list[Overlay]: """The overlays that judge one file. Rule-only overlays that target the scenario or its family judge every file of the scenario. An overlay with a ``patch`` judges only the variant built with that patch. Args: scenario: The scenario. pool: Every loaded overlay. variant: The overlay id the file was built with, or ``None`` for the generic file. version: The edition of the file; overlays that do not cover it are left out. Returns: The overlays to evaluate, in pool order. """ wanted = scenario.overlays chosen: list[Overlay] = [] for overlay in pool: if ( version is not None and overlay.versions and version not in overlay.versions ): continue if wanted is None: if not overlay.applies(scenario.id, scenario.family): continue elif overlay.overlay_id not in wanted: continue if overlay.has_patch and overlay.overlay_id != variant: continue chosen.append(overlay) return chosen
[docs] def run_ladder( scenario: Scenario, version: str, xml: str, overlays: list[Overlay] | None = None, variant: str | None = None, ) -> dict[str, Any]: """Run L0 to L3 on one built file. Args: scenario: The scenario the file was built from. version: The edition. xml: The built document. overlays: The overlay pool; defaults to ``scenarios/overlays``. variant: The overlay id the file was built with, if any. Returns: ``{"xsd": ..., "mdr": ..., "profiles": {...}, "overlays": {...}}`` with error and warning counts and the first messages of each. """ xsd = str(DEFAULT_TEMPLATE_REGISTRY.get_template(version).xsd_path) xsd_errors = collect_xsd_validation_errors(xml, xsd, max_errors=5) mdr = evaluate_mdr(xml) rows = rows_from_xml(xml) profiles: dict[str, Any] = {} for name in scenario.profiles: result = validate_scheme(rows, name) errors = [v for v in result.violations if v.severity == "error"] warnings = [v for v in result.violations if v.severity != "error"] profiles[name] = { "errors": len(errors), "warnings": len(warnings), "findings": [ f"{v.rule}: {v.message}" for v in result.violations[:5] ], } pool = load_overlays() if overlays is None else overlays applied: dict[str, Any] = {} for overlay in applicable_overlays(scenario, pool, variant, version): findings = evaluate(overlay, xml) applied[overlay.overlay_id] = { "errors": sum(f.severity == "error" for f in findings), "warnings": sum(f.severity != "error" for f in findings), "findings": [f"{f.code}: {f.message}" for f in findings[:5]], } return { "xsd": {"errors": len(xsd_errors), "findings": xsd_errors}, "mdr": { "errors": len(mdr), "findings": [f"{f.rule_id} at {f.path}" for f in mdr[:5]], }, "profiles": profiles, "overlays": applied, }
[docs] def ladder_passes(record: dict[str, Any]) -> bool: """True when no rung of a :func:`run_ladder` record has an error.""" if record["xsd"]["errors"] or record["mdr"]["errors"]: return False for rung in ("profiles", "overlays"): if any(entry["errors"] for entry in record[rung].values()): return False return True
__all__ = ["applicable_overlays", "ladder_passes", "run_ladder"]