← Files ClaraARCHIVED FILE

modules/reporting-engine/scripts/reporting_adapters.py

14.1 KB · Oct 3, 2026 · 06:30 UTC

↓ Download file

"""Resolve Clara reporting-engine adapters for chart capabilities."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from check_compatibility import check_capability_compatibility

__all__ = [
    "adapter_registry_path",
    "catalog_root",
    "component_root",
    "list_adapters",
    "load_adapter_registry",
    "load_manifest",
    "prepare_invocation_plan",
    "reporting_engine_root",
    "resolve_capability_adapter",
    "summarize_adapters",
    "main",
]


def reporting_engine_root() -> Path:
    """Return the editable or packaged reporting-engine component root."""

    return Path(__file__).resolve().parents[1]


def catalog_root(root: Path | None = None) -> Path:
    """Return the reporting-engine catalog directory."""

    return (root or reporting_engine_root()) / "catalog"


def adapter_registry_path(root: Path | None = None) -> Path:
    """Return the adapter registry path."""

    return catalog_root(root) / "adapter_registry.json"


def _load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def load_adapter_registry(root: Path | None = None) -> dict[str, Any]:
    """Load the Clara reporting adapter registry."""

    return _load_json(adapter_registry_path(root))


def load_manifest(root: Path | None = None) -> dict[str, Any]:
    """Load the chart-selection manifest."""

    return _load_json(catalog_root(root) / "selection_manifest.json")


def component_root(component_name: str, root: Path | None = None) -> Path:
    """Return the embedded Clara component root or repository fallback root."""

    base = root or reporting_engine_root()
    sibling = base.parent / component_name
    if sibling.is_dir():
        return sibling
    if base.parent.name == "modules" and base.parent.parent.name == "clara":
        repository_component = base.parents[2] / component_name
        if repository_component.is_dir():
            return repository_component
    return sibling


def list_adapters(root: Path | None = None) -> list[dict[str, Any]]:
    """Return adapter records sorted by adapter id."""

    registry = load_adapter_registry(root)
    adapters = registry.get("adapters") or {}
    return sorted(
        (dict(adapter) for adapter in adapters.values()),
        key=lambda adapter: str(adapter.get("adapter_id") or ""),
    )


def _adapter_for_legacy_source(
    legacy_source: str, registry: dict[str, Any]
) -> dict[str, Any]:
    adapters = registry.get("adapters") or {}
    adapter = adapters.get(legacy_source)
    if not isinstance(adapter, dict):
        raise KeyError(f"No reporting-engine adapter registered for {legacy_source}")
    return adapter


def _capability_record(capability_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
    capabilities = manifest.get("capabilities") or {}
    capability = capabilities.get(capability_id)
    if not isinstance(capability, dict):
        raise KeyError(f"Unknown capability: {capability_id}")
    return capability


def _legacy_source_for_capability(capability: dict[str, Any]) -> str:
    contract = capability.get("normalized_invocation_contract") or {}
    plugin_sources = contract.get("plugin_sources") or []
    if len(plugin_sources) != 1:
        raise ValueError(
            "Capability must have exactly one legacy plugin source to resolve "
            f"a reporting adapter: {plugin_sources}"
        )
    return str(plugin_sources[0])


def resolve_capability_adapter(
    capability_id: str,
    *,
    root: Path | None = None,
) -> dict[str, Any]:
    """Return the Clara adapter record for one manifest capability."""

    registry = load_adapter_registry(root)
    manifest = load_manifest(root)
    capability = _capability_record(capability_id, manifest)
    legacy_source = _legacy_source_for_capability(capability)
    adapter = dict(_adapter_for_legacy_source(legacy_source, registry))
    contract = capability.get("normalized_invocation_contract") or {}
    manifest_adapter = contract.get("clara_adapter") or {}
    resolved_root = component_root(str(adapter["component_name"]), root)
    adapter.update(
        {
            "capability_id": capability_id,
            "component_root": str(resolved_root),
            "component_exists": resolved_root.is_dir(),
            "manifest_adapter_id": manifest_adapter.get("adapter_id"),
            "manifest_adapter_matches_registry": (
                manifest_adapter.get("adapter_id") == adapter.get("adapter_id")
            ),
        }
    )
    return adapter


def _dataset_candidates_for_role(
    dataset_profile: dict[str, Any] | None,
    role_contract: dict[str, Any],
) -> list[str]:
    if not dataset_profile:
        return []
    role_candidate_columns = dataset_profile.get("role_candidate_columns") or {}
    role_candidates = dataset_profile.get("role_candidates") or {}
    role = str(role_contract.get("role") or "")
    if role in role_candidate_columns:
        return [str(item) for item in role_candidate_columns.get(role) or []]
    if role in role_candidates:
        return _candidate_columns(role_candidates.get(role) or [])
    kind = str(role_contract.get("kind") or "")
    fallback_roles = {
        "period": "period_axis",
        "metric": "comparison_metric",
        "dimension": "dimension_member",
        "identifier": "identifier",
    }
    fallback = fallback_roles.get(kind)
    if fallback is None:
        return []
    if fallback in role_candidate_columns:
        return [str(item) for item in role_candidate_columns.get(fallback) or []]
    candidates = _candidate_columns(role_candidates.get(fallback) or [])
    if candidates:
        return candidates
    source_roles = dataset_profile.get("roles") or {}
    source_role = {
        "period_axis": "period",
        "comparison_metric": "metric",
        "dimension_member": "dimension",
        "identifier": "identifier",
    }.get(fallback)
    return (
        [str(item) for item in source_roles.get(source_role, [])] if source_role else []
    )


def _candidate_columns(candidates: list[Any]) -> list[str]:
    columns: list[str] = []
    for candidate in candidates:
        if isinstance(candidate, dict) and candidate.get("column"):
            columns.append(str(candidate["column"]))
        elif isinstance(candidate, str):
            columns.append(candidate)
    return columns


def _compatibility_matches(
    compatibility: dict[str, Any] | None,
) -> dict[tuple[str, str], dict[str, Any]]:
    if not compatibility:
        return {}
    return {
        (str(match.get("kind") or ""), str(match.get("role") or "")): match
        for match in compatibility.get("mechanical_role_matches") or []
        if isinstance(match, dict)
    }


def _role_plan(
    contracts: list[dict[str, Any]],
    dataset_profile: dict[str, Any] | None,
    compatibility: dict[str, Any] | None,
) -> list[dict[str, Any]]:
    plan: list[dict[str, Any]] = []
    matches = _compatibility_matches(compatibility)
    for contract in contracts:
        kind = str(contract.get("kind") or "")
        role = str(contract.get("role") or "")
        match = matches.get((kind, role))
        candidates = (
            [str(item) for item in match.get("candidate_columns") or []]
            if match
            else _dataset_candidates_for_role(dataset_profile, contract)
        )
        if dataset_profile is None:
            dataset_match_status = "not_checked"
        elif match and match.get("fit_status") == "semantic_or_package_gap":
            dataset_match_status = "semantic_or_package_gap"
        elif match and match.get("fit_status") == "optional_not_matched":
            dataset_match_status = "optional_not_available"
        elif match and match.get("fit_status") != "satisfied":
            dataset_match_status = "missing_candidate"
        elif match and str(match.get("resolution_type") or "").startswith("derived_"):
            dataset_match_status = "derivable"
        elif match and match.get("resolution_type") == "structural_variance_step":
            dataset_match_status = "derivable"
        elif match and match.get("ambiguity_status") == "ambiguous":
            dataset_match_status = "candidate_ambiguous"
        elif candidates:
            dataset_match_status = "candidate_available"
        else:
            dataset_match_status = "missing_candidate"
        plan.append(
            {
                "kind": kind,
                "role": role,
                "required": bool(contract.get("required", True)),
                "mapping_kind": contract.get("mapping_kind"),
                "status": contract.get("status"),
                "parameter_targets": contract.get("parameter_targets") or [],
                "dataset_candidates": candidates,
                "dataset_match_status": dataset_match_status,
                "fit_status": match.get("fit_status") if match else None,
                "ambiguity_status": (match.get("ambiguity_status") if match else None),
                "resolution_type": match.get("resolution_type") if match else None,
                "issue": match.get("issue") if match else None,
                "scope_binding": contract.get("scope_binding"),
            }
        )
    return plan


def prepare_invocation_plan(
    capability_id: str,
    *,
    dataset_profile: dict[str, Any] | None = None,
    root: Path | None = None,
) -> dict[str, Any]:
    """Return a mechanical plan for rendering one chosen capability."""

    manifest = load_manifest(root)
    capability = _capability_record(capability_id, manifest)
    contract = capability.get("normalized_invocation_contract") or {}
    adapter = resolve_capability_adapter(capability_id, root=root)
    required_contracts = contract.get("required_role_contracts") or []
    optional_contracts = contract.get("optional_role_contracts") or []
    variant_contracts = contract.get("variant_role_contracts") or []
    compatibility = (
        check_capability_compatibility(
            capability_id,
            dataset_profile,
            manifest=manifest,
        )
        if dataset_profile is not None
        else None
    )
    return {
        "schema_version": "0.1",
        "capability_id": capability_id,
        "owner": "clara.reporting-engine",
        "adapter_id": adapter.get("adapter_id"),
        "component_name": adapter.get("component_name"),
        "component_root": adapter.get("component_root"),
        "component_exists": adapter.get("component_exists"),
        "legacy_plugin_source": adapter.get("legacy_plugin_source"),
        "legacy_plugin_source_policy": (
            "Provenance only; Clara callers should invoke the reporting-engine "
            "adapter contract."
        ),
        "render_api_status": adapter.get("render_api_status"),
        "entrypoints": adapter.get("entrypoints") or {},
        "required_roles": _role_plan(
            required_contracts, dataset_profile, compatibility
        ),
        "optional_roles": _role_plan(
            optional_contracts, dataset_profile, compatibility
        ),
        "variant_roles": _role_plan(variant_contracts, dataset_profile, None),
        "compatibility": compatibility,
        "period_scope": capability.get("period_scope_contract") or {},
        "artifact_invocation_contracts": (
            contract.get("artifact_invocation_contracts") or []
        ),
        "output_forms": contract.get("output_forms") or [],
        "dataset_checked": dataset_profile is not None,
        "mechanical_status": contract.get("status"),
        "boundary": (
            "Mechanical adapter and parameter plan only. It does not choose a "
            "semantically valid analysis."
        ),
    }


def summarize_adapters(root: Path | None = None) -> dict[str, Any]:
    """Return a compact adapter registry summary."""

    registry = load_adapter_registry(root)
    manifest = load_manifest(root)
    adapters = registry.get("adapters") or {}
    capability_counts: dict[str, int] = {source: 0 for source in adapters}
    missing_sources: set[str] = set()
    for capability in (manifest.get("capabilities") or {}).values():
        if not isinstance(capability, dict):
            continue
        source = _legacy_source_for_capability(capability)
        if source in capability_counts:
            capability_counts[source] += 1
        else:
            missing_sources.add(source)
    return {
        "schema_version": "0.1",
        "owner": registry.get("owner"),
        "adapter_count": len(adapters),
        "adapter_ids": sorted(
            str(adapter.get("adapter_id")) for adapter in adapters.values()
        ),
        "component_names": sorted(
            str(adapter.get("component_name")) for adapter in adapters.values()
        ),
        "capability_counts_by_legacy_source": dict(sorted(capability_counts.items())),
        "missing_registry_sources": sorted(missing_sources),
        "legacy_plugin_source_policy": registry.get("legacy_plugin_source_policy"),
    }


def main(argv: list[str] | None = None) -> int:
    """Print the adapter summary or a resolved capability plan."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--catalog-root",
        type=Path,
        default=None,
        help="Override catalog directory or reporting-engine root.",
    )
    parser.add_argument("--capability", help="Capability id to resolve.")
    parser.add_argument(
        "--plan",
        action="store_true",
        help="Print the invocation plan instead of only the adapter record.",
    )
    args = parser.parse_args(argv)
    root = None
    if args.catalog_root is not None:
        root = (
            args.catalog_root.parent
            if args.catalog_root.name == "catalog"
            else args.catalog_root
        )
    if args.capability:
        payload = (
            prepare_invocation_plan(args.capability, root=root)
            if args.plan
            else resolve_capability_adapter(args.capability, root=root)
        )
    else:
        payload = summarize_adapters(root)
    print(json.dumps(payload, indent=2, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: d33dfa1f4bd029e2033c8444d4c5c09444d082bd54a9f67e279cf11e49013104