CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/speaker-toolkit

Six-skill presentation system: ingest talks into a rhetoric vault, run interactive clarification, generate a speaker profile, create presentations that match your documented patterns, produce the deck illustrations + thumbnail visual layer, and publish talk pages to a Jekyll shownotes site. Includes a 111-entry Presentation Patterns taxonomy (81 observable: 62 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns) for scoring, brainstorming, and go-live preparation.

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

profile_pattern_provenance.pyskills/vault-profile/scripts/

"""Strict reusable contract for profile pattern history.

Schema-v4 pattern profiles remain readable as occurrence-only inputs with every
classification domain disabled. Schema-v5 profiles add a self-contained,
policy-bound classification contract with independent domain availability.
This module validates provenance and projections; classification arithmetic is
owned only by ``classify-pattern-profile.py``.
"""

from __future__ import annotations

import math
import pathlib
import sys
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from fractions import Fraction
from typing import TypeGuard


_INGRESS_SCRIPTS = (
    pathlib.Path(__file__).resolve().parents[2] / "vault-ingress" / "scripts"
)
if str(_INGRESS_SCRIPTS) not in sys.path:
    sys.path.insert(0, str(_INGRESS_SCRIPTS))

# Pyright cannot resolve this sibling script module added to sys.path at runtime.
from adherence_baseline import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    AdherenceBaselineError,
    validate_adherence_baseline,
)
from pattern_classification_runtime import validate_policy_stamp  # noqa: E402
from pattern_opportunities import (  # noqa: E402
    PatternOpportunityError,
    validate_pattern_opportunity_rows,
)

# Pyright cannot resolve this sibling script module added to sys.path at runtime.
from return_validation import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    PATTERN_SCORING_SCHEMA_VERSION,
    ReturnValidationError,
    load_catalog,
)


REASON_INVALID_CONTRACT = "invalid_pattern_profile_contract"
REASON_ACTIVE_CATALOG_UNAVAILABLE = "active_pattern_catalog_unavailable"
REASON_CATALOG_FINGERPRINT_MISMATCH = "pattern_catalog_fingerprint_mismatch"
REASON_SCORING_SCHEMA_MISMATCH = "pattern_scoring_schema_mismatch"
REASON_EMPTY_CURRENT_COHORT = "empty_current_pattern_cohort"
REASON_CLASSIFICATION_POLICY_UNAVAILABLE = "pattern_classification_policy_unavailable"
REASON_CLASSIFICATION_POLICY_INVALID = "pattern_classification_policy_invalid"

LEGACY_CLASSIFICATION_AVAILABILITY_SCHEMA_VERSION = 1
CLASSIFICATION_AVAILABILITY_SCHEMA_VERSION = 2
CLASSIFICATION_SCHEMA_VERSION = 1
CLASSIFICATION_POLICY_UNAVAILABLE_REASON = "owner_policy_unconfigured"

_LEGACY_AVAILABILITY_FIELDS = frozenset({"schema_version", "status", "reason_codes"})
CLASSIFICATION_DOMAINS = (
    "mastery_and_novelty",
    "antipattern_recurrence",
    "underuse",
    "signature_combinations",
    "trends",
    "modes",
)
_DOMAIN_STATUS_FIELDS = frozenset({"status", "reason_codes"})
_AVAILABILITY_V2_FIELDS = frozenset({"schema_version", *CLASSIFICATION_DOMAINS})

_COMMON_PATTERN_PROFILE_FIELDS = frozenset(
    {
        "pattern_baseline",
        "baseline_talk_filenames",
        "eligible_talk_count",
        "talks_scored",
        "average_pattern_score",
        "score_trend",
        "pattern_breadth",
        "underused_patterns",
        "score_drivers",
        "by_mode",
        "strengths",
        "strengths_note",
        "note",
        "pattern_usage",
        "antipattern_frequency",
        "never_used_patterns",
        "signature_combinations",
        "mastery_levels",
        "classification_availability",
    }
)
_V4_PATTERN_PROFILE_FIELDS = _COMMON_PATTERN_PROFILE_FIELDS
_V5_PATTERN_PROFILE_FIELDS = _COMMON_PATTERN_PROFILE_FIELDS | frozenset(
    {
        "classification_schema_version",
        "classification_policy",
        "pattern_classifications",
        "antipattern_classifications",
        "trend_analysis",
    }
)
_REQUIRED_PATTERN_BREADTH_FIELDS = frozenset(
    {"avg_distinct_patterns_per_talk", "trend", "note"}
)
_REQUIRED_SCORE_DRIVER_FIELDS = frozenset(
    {"direction", "antipattern_drivers", "pattern_drivers", "note"}
)
_ARRAY_PATTERN_PROFILE_FIELDS = (
    "underused_patterns",
    "by_mode",
    "strengths",
    "pattern_usage",
    "antipattern_frequency",
    "never_used_patterns",
    "signature_combinations",
)
_DERIVED_ARRAY_PATTERN_PROFILE_FIELDS = (
    "underused_patterns",
    "by_mode",
    "strengths",
    "never_used_patterns",
    "signature_combinations",
)
_MASTERY_TIERS = (
    "signature",
    "regular",
    "occasional",
    "rare",
    "never_tried",
)
_POSITIVE_CLASSIFICATIONS = frozenset(
    {
        "signature",
        "regular",
        "occasional",
        "rare",
        "never_tried",
        "not_yet_observed",
        "unclassified",
    }
)
_ANTIPATTERN_CLASSIFICATIONS = frozenset(
    {
        "high_frequency",
        "moderate_frequency",
        "occasional",
        "confirmed_none",
        "unclassified",
    }
)
_OBSERVATION_STATUSES = frozenset(
    {"observed", "confirmed_absent", "not_yet_observed", "unavailable"}
)
_CLASSIFICATION_ROW_FIELDS = frozenset(
    {
        "pattern_id",
        "classification",
        "observation_status",
        "absence_conclusion_capable",
        "evidence",
        "reason_codes",
    }
)
_EVIDENCE_COUNT_FIELDS = (
    "applicable_count",
    "evaluable_count",
    "detected_count",
    "unevaluable_count",
)
_EVIDENCE_RATE_FIELDS = ("applicable_coverage", "lower", "upper")
_EVIDENCE_FIELDS = frozenset((*_EVIDENCE_COUNT_FIELDS, *_EVIDENCE_RATE_FIELDS))
_COMBINATION_FIELDS = frozenset(
    {"combination_id", "pattern_ids", "evidence", "reason_codes"}
)
_TREND_ANALYSIS_FIELDS = frozenset(
    {
        "status",
        "reason_codes",
        "sample",
        "score",
        "breadth",
        "pattern_movements",
        "antipattern_movements",
    }
)
_TREND_SAMPLE_FIELDS = frozenset(
    {
        "required_talk_count",
        "valid_date_talk_count",
        "invalid_date_filenames",
        "selected_filenames",
        "opportunity_coverage_identity",
    }
)
_TREND_METRIC_FIELDS = frozenset({"status", "prior_average", "recent_average", "delta"})
_TREND_MOVEMENT_FIELDS = frozenset(
    {"pattern_id", "movement", "prior_evidence", "recent_evidence", "reason_codes"}
)
_TREND_UNAVAILABLE_REASONS = frozenset(
    {
        "insufficient_valid_date_sample",
        "opportunity_identity_unavailable",
        "incomparable_opportunity_identities",
        "no_evaluable_pattern_opportunities",
    }
)
_MOVEMENT_REASON_CODES = {
    "increasing": ("conservative_interval_increase",),
    "decreasing": ("conservative_interval_decrease",),
    "stable": ("conservative_interval_stable",),
    "indeterminate": ("uncertainty_spans_movement_threshold",),
}
_UNAVAILABLE_MOVEMENT_REASON_CODES = frozenset(
    {"incomplete_window_applicability", "window_bounds_unavailable"}
)
_LOWER_HEX_CHARS = frozenset("0123456789abcdef")


@dataclass(frozen=True)
class PatternProfileAssessment:
    """Availability and diagnostics for one nested pattern profile."""

    current_contract: bool
    catalog_fields_available: bool
    scored_talk_count: int | None
    reason_codes: tuple[str, ...]
    errors: tuple[str, ...]
    eligible_talk_count: int | None = None
    classification_fields_available: bool = False
    available_classification_domains: frozenset[str] = frozenset()
    contract_version: int | None = None
    policy_semantic_sha256: str | None = None

    def domain_available(self, domain: str) -> bool:
        """Return whether one policy-derived domain is independently enabled."""
        return domain in self.available_classification_domains


def active_pattern_generation_identity() -> tuple[str, int]:
    """Return the exact bundled catalog fingerprint and scoring schema."""
    return load_catalog().fingerprint, PATTERN_SCORING_SCHEMA_VERSION


def unavailable_classification_availability() -> dict[str, object]:
    """Return the schema-v4 occurrence-only classification sentinel."""
    return {
        "schema_version": LEGACY_CLASSIFICATION_AVAILABILITY_SCHEMA_VERSION,
        "status": "unavailable",
        "reason_codes": [CLASSIFICATION_POLICY_UNAVAILABLE_REASON],
    }


def _is_integer(value: object) -> TypeGuard[int]:
    return isinstance(value, int) and not isinstance(value, bool)


def _is_optional_unit_interval(value: object) -> bool:
    return value is None or (
        not isinstance(value, bool)
        and isinstance(value, (int, float))
        and 0 <= value <= 1
        and math.isfinite(value)
    )


def _is_finite_number(value: object) -> TypeGuard[int | float]:
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        return False
    return not isinstance(value, float) or math.isfinite(value)


def _is_lower_hex64(value: object) -> TypeGuard[str]:
    return (
        isinstance(value, str)
        and len(value) == 64
        and all(character in _LOWER_HEX_CHARS for character in value)
    )


def _unit_ratio(numerator: int, denominator: int) -> float:
    return float(Fraction(numerator, denominator))


def _append_reason(reason_codes: list[str], reason_code: str) -> None:
    if reason_code not in reason_codes:
        reason_codes.append(reason_code)


def _exact_fields(
    value: Mapping[object, object], expected: frozenset[str], *, path: str
) -> list[str]:
    missing = sorted(field for field in expected if field not in value)
    unknown = sorted((field for field in value if field not in expected), key=str)
    if not missing and not unknown:
        return []
    return [
        f"{path} fields are noncanonical; missing={missing}, "
        f"unknown={[str(item) for item in unknown]}"
    ]


def _string_array(
    value: object, *, path: str, sorted_required: bool = False
) -> list[str]:
    if not isinstance(value, list):
        return [f"{path} must be an array"]
    errors: list[str] = []
    strings: list[str] = []
    for index, item in enumerate(value):
        if not isinstance(item, str) or not item or item != item.strip():
            errors.append(
                f"{path}[{index}] must be a non-empty string without edge whitespace"
            )
        else:
            strings.append(item)
    if len(strings) != len(set(strings)):
        errors.append(f"{path} must not contain duplicates")
    if sorted_required and strings != sorted(strings):
        errors.append(f"{path} must be sorted")
    return errors


def _canonical_cohort_filenames(value: object) -> tuple[list[str] | None, list[str]]:
    errors = _string_array(
        value, path="pattern_profile.baseline_talk_filenames", sorted_required=True
    )
    if errors or not isinstance(value, list):
        return None, errors
    return list(value), []


def _validate_common_shape(pattern_profile: Mapping[str, object]) -> list[str]:
    errors: list[str] = []
    for field in _ARRAY_PATTERN_PROFILE_FIELDS:
        if field in pattern_profile and not isinstance(pattern_profile[field], list):
            errors.append(f"pattern_profile.{field} must be an array")
    for field in ("strengths_note", "note"):
        if field in pattern_profile and not isinstance(pattern_profile[field], str):
            errors.append(f"pattern_profile.{field} must be a string")

    breadth = pattern_profile.get("pattern_breadth")
    if isinstance(breadth, Mapping):
        missing_breadth = sorted(_REQUIRED_PATTERN_BREADTH_FIELDS - set(breadth))
        unknown_breadth = sorted(
            set(breadth) - _REQUIRED_PATTERN_BREADTH_FIELDS, key=str
        )
        if missing_breadth:
            errors.append(
                "pattern_profile.pattern_breadth is missing required fields: "
                + ", ".join(missing_breadth)
            )
        if unknown_breadth:
            errors.append(
                "pattern_profile.pattern_breadth has unknown fields: "
                + ", ".join(str(field) for field in unknown_breadth)
            )
        if "note" in breadth and not isinstance(breadth["note"], str):
            errors.append("pattern_profile.pattern_breadth.note must be a string")
    else:
        errors.append("pattern_profile.pattern_breadth must be an object")

    drivers = pattern_profile.get("score_drivers")
    if isinstance(drivers, Mapping):
        errors.extend(
            _exact_fields(
                drivers,
                _REQUIRED_SCORE_DRIVER_FIELDS,
                path="pattern_profile.score_drivers",
            )
        )
        for field in ("antipattern_drivers", "pattern_drivers"):
            if field in drivers:
                errors.extend(
                    _string_array(
                        drivers[field],
                        path=f"pattern_profile.score_drivers.{field}",
                        sorted_required=True,
                    )
                )
        if "note" in drivers and not isinstance(drivers["note"], str):
            errors.append("pattern_profile.score_drivers.note must be a string")
    else:
        errors.append("pattern_profile.score_drivers must be an object")

    mastery = pattern_profile.get("mastery_levels")
    if isinstance(mastery, Mapping):
        missing_tiers = sorted(set(_MASTERY_TIERS) - set(mastery))
        unknown_tiers = sorted(set(mastery) - set(_MASTERY_TIERS), key=str)
        if missing_tiers:
            errors.append(
                "pattern_profile.mastery_levels is missing required tiers: "
                + ", ".join(missing_tiers)
            )
        if unknown_tiers:
            errors.append(
                "pattern_profile.mastery_levels has unknown tiers: "
                + ", ".join(str(field) for field in unknown_tiers)
            )
        for tier in _MASTERY_TIERS:
            if tier in mastery:
                errors.extend(
                    _string_array(
                        mastery[tier],
                        path=f"pattern_profile.mastery_levels.{tier}",
                        sorted_required=True,
                    )
                )
    else:
        errors.append("pattern_profile.mastery_levels must be an object")
    return errors


def _validate_v4_unavailable(pattern_profile: Mapping[str, object]) -> list[str]:
    """Require fail-closed schema-v4 derived sentinels."""
    errors: list[str] = []
    availability = pattern_profile.get("classification_availability")
    expected = unavailable_classification_availability()
    if not isinstance(availability, Mapping):
        errors.append("pattern_profile.classification_availability must be an object")
    else:
        errors.extend(
            _exact_fields(
                availability,
                _LEGACY_AVAILABILITY_FIELDS,
                path="pattern_profile.classification_availability",
            )
        )
        if dict(availability) != expected:
            errors.append(
                "pattern_profile.classification_availability must use the "
                "owner-policy-unconfigured schema-v4 sentinel"
            )
    if pattern_profile.get("score_trend") != "unavailable":
        errors.append(
            "pattern_profile.score_trend must be 'unavailable' while pattern "
            "classification policy is unavailable"
        )
    breadth = pattern_profile.get("pattern_breadth")
    if isinstance(breadth, Mapping):
        if breadth.get("avg_distinct_patterns_per_talk") is not None:
            errors.append(
                "pattern_profile.pattern_breadth.avg_distinct_patterns_per_talk "
                "must be null while coverage-comparable breadth policy is unavailable"
            )
        if breadth.get("trend") != "unavailable":
            errors.append(
                "pattern_profile.pattern_breadth.trend must be 'unavailable' when "
                "pattern classification policy is unavailable"
            )
    drivers = pattern_profile.get("score_drivers")
    if isinstance(drivers, Mapping):
        if drivers.get("direction") != "unavailable":
            errors.append(
                "pattern_profile.score_drivers.direction must be 'unavailable' "
                "while pattern classification policy is unavailable"
            )
        for field in ("antipattern_drivers", "pattern_drivers"):
            if drivers.get(field) != []:
                errors.append(
                    f"pattern_profile.score_drivers.{field} must be [] when the "
                    "pattern classification policy is unavailable"
                )
    for field in _DERIVED_ARRAY_PATTERN_PROFILE_FIELDS:
        if pattern_profile.get(field) != []:
            errors.append(
                f"pattern_profile.{field} must be [] while pattern classification "
                "policy is unavailable"
            )
    mastery = pattern_profile.get("mastery_levels")
    if isinstance(mastery, Mapping):
        for tier in _MASTERY_TIERS:
            if mastery.get(tier) != []:
                errors.append(
                    f"pattern_profile.mastery_levels.{tier} must be [] while "
                    "pattern classification policy is unavailable"
                )
    return errors


def _validate_availability_v2(
    value: object,
) -> tuple[frozenset[str], list[str]]:
    if not isinstance(value, Mapping):
        return frozenset(), [
            "pattern_profile.classification_availability must be an object"
        ]
    errors = _exact_fields(
        value,
        _AVAILABILITY_V2_FIELDS,
        path="pattern_profile.classification_availability",
    )
    availability_schema_version = value.get("schema_version")
    if (
        not _is_integer(availability_schema_version)
        or availability_schema_version != CLASSIFICATION_AVAILABILITY_SCHEMA_VERSION
    ):
        errors.append(
            "pattern_profile.classification_availability.schema_version must be 2"
        )
    available: set[str] = set()
    for domain in CLASSIFICATION_DOMAINS:
        item = value.get(domain)
        path = f"pattern_profile.classification_availability.{domain}"
        if not isinstance(item, Mapping):
            errors.append(f"{path} must be an object")
            continue
        errors.extend(_exact_fields(item, _DOMAIN_STATUS_FIELDS, path=path))
        status = item.get("status")
        if status not in {"available", "unavailable"}:
            errors.append(f"{path}.status must be available or unavailable")
        reason_codes = item.get("reason_codes")
        errors.extend(_string_array(reason_codes, path=f"{path}.reason_codes"))
        if status == "available":
            available.add(domain)
            if reason_codes != []:
                errors.append(f"{path}.reason_codes must be [] when available")
        elif status == "unavailable" and reason_codes == []:
            errors.append(f"{path}.reason_codes must explain unavailability")
    modes = value.get("modes")
    if isinstance(modes, Mapping) and modes != {
        "status": "unavailable",
        "reason_codes": ["talk_mode_assignments_unavailable"],
    }:
        errors.append(
            "pattern_profile.classification_availability.modes must fail closed "
            "with talk_mode_assignments_unavailable"
        )
    return frozenset(available), errors


def _expected_evidence(raw: Mapping[str, object]) -> dict[str, object] | None:
    fields = (
        "eligible_cohort_count",
        "not_applicable_count",
        "evaluable_count",
        "detected_count",
        "unevaluable_count",
    )
    counts: dict[str, int] = {}
    for field in fields:
        value = raw.get(field)
        if not _is_integer(value) or value < 0:
            return None
        counts[field] = value
    eligible = counts["eligible_cohort_count"]
    not_applicable = counts["not_applicable_count"]
    evaluable = counts["evaluable_count"]
    detected = counts["detected_count"]
    unevaluable = counts["unevaluable_count"]
    applicable = eligible - not_applicable
    if applicable < 0 or evaluable + unevaluable != applicable or detected > evaluable:
        return None
    if applicable == 0:
        coverage = lower = upper = None
    else:
        coverage = _unit_ratio(evaluable, applicable)
        lower = _unit_ratio(detected, applicable)
        upper = _unit_ratio(detected + unevaluable, applicable)
    return {
        "applicable_count": applicable,
        "evaluable_count": evaluable,
        "detected_count": detected,
        "unevaluable_count": unevaluable,
        "applicable_coverage": coverage,
        "lower": lower,
        "upper": upper,
    }


def _validate_evidence(value: Mapping[object, object], *, path: str) -> list[str]:
    errors = _exact_fields(value, _EVIDENCE_FIELDS, path=path)
    counts: dict[str, int] = {}
    for field in _EVIDENCE_COUNT_FIELDS:
        count = value.get(field)
        if not _is_integer(count) or count < 0:
            errors.append(f"{path}.{field} must be a non-negative integer")
        else:
            counts[field] = count

    for field in _EVIDENCE_RATE_FIELDS:
        if not _is_optional_unit_interval(value.get(field)):
            errors.append(
                f"{path}.{field} must be null or a finite number between zero and one"
            )

    if len(counts) != len(_EVIDENCE_COUNT_FIELDS):
        return errors
    applicable = counts["applicable_count"]
    evaluable = counts["evaluable_count"]
    detected = counts["detected_count"]
    unevaluable = counts["unevaluable_count"]
    if evaluable + unevaluable != applicable:
        errors.append(f"{path} must satisfy applicable_count = E + U")
        return errors
    if detected > evaluable:
        errors.append(f"{path}.detected_count cannot exceed evaluable_count")
        return errors

    expected_rates: dict[str, float | None]
    if applicable == 0:
        expected_rates = {
            "applicable_coverage": None,
            "lower": None,
            "upper": None,
        }
    else:
        expected_rates = {
            "applicable_coverage": _unit_ratio(evaluable, applicable),
            "lower": _unit_ratio(detected, applicable),
            "upper": _unit_ratio(detected + unevaluable, applicable),
        }
    for field, expected in expected_rates.items():
        actual = value.get(field)
        if _is_optional_unit_interval(actual) and actual != expected:
            errors.append(
                f"{path}.{field} must equal the canonical ratio {expected!r}, "
                f"got {actual!r}"
            )
    return errors


def _validate_classification_lane(
    value: object,
    raw_rows: object,
    *,
    lane: str,
    allowed_classifications: frozenset[str],
    active_catalog: object,
) -> tuple[list[Mapping[str, object]], list[str]]:
    errors: list[str] = []
    if not isinstance(value, list):
        return [], [f"pattern_profile.{lane} must be an array"]
    if not isinstance(raw_rows, list):
        return [], [f"pattern_profile.{lane} cannot bind missing raw rows"]
    raw_by_id = {
        row.get("pattern_id"): row
        for row in raw_rows
        if isinstance(row, Mapping) and isinstance(row.get("pattern_id"), str)
    }
    expected_ids = list(raw_by_id)
    observed_ids: list[str] = []
    canonical: list[Mapping[str, object]] = []
    entries = getattr(active_catalog, "entries", {})
    for index, item in enumerate(value):
        path = f"pattern_profile.{lane}[{index}]"
        if not isinstance(item, Mapping):
            errors.append(f"{path} must be an object")
            continue
        errors.extend(_exact_fields(item, _CLASSIFICATION_ROW_FIELDS, path=path))
        pattern_id = item.get("pattern_id")
        if not isinstance(pattern_id, str) or not pattern_id:
            errors.append(f"{path}.pattern_id must be a non-empty string")
            continue
        canonical.append(item)
        observed_ids.append(pattern_id)
        classification = item.get("classification")
        if classification not in allowed_classifications:
            errors.append(f"{path}.classification is invalid: {classification!r}")
        observation_status = item.get("observation_status")
        if observation_status not in _OBSERVATION_STATUSES:
            errors.append(
                f"{path}.observation_status is invalid: {observation_status!r}"
            )
        absence_capable = item.get("absence_conclusion_capable")
        entry = entries.get(pattern_id) if isinstance(entries, Mapping) else None
        expected_absence = (
            getattr(entry, "absence_evaluable_from", None) is not None
            if entry is not None
            else None
        )
        if not isinstance(absence_capable, bool) or absence_capable != expected_absence:
            errors.append(
                f"{path}.absence_conclusion_capable must match active catalog metadata"
            )
        evidence = item.get("evidence")
        applicable_count: object = None
        detected_count: object = None
        if not isinstance(evidence, Mapping):
            errors.append(f"{path}.evidence must be an object")
        else:
            errors.extend(_validate_evidence(evidence, path=f"{path}.evidence"))
            applicable_count = evidence.get("applicable_count")
            detected_count = evidence.get("detected_count")
            raw = raw_by_id.get(pattern_id)
            expected_evidence = (
                _expected_evidence(raw) if isinstance(raw, Mapping) else None
            )
            if expected_evidence is not None and dict(evidence) != expected_evidence:
                errors.append(
                    f"{path}.evidence does not match the exact raw A/E/D/U row"
                )
        errors.extend(
            _string_array(item.get("reason_codes"), path=f"{path}.reason_codes")
        )
        if item.get("reason_codes") == []:
            errors.append(f"{path}.reason_codes must not be empty")
        if absence_capable is False and classification in {
            "never_tried",
            "confirmed_none",
        }:
            errors.append(
                f"{path}.classification cannot conclude absence for a positive-only entry"
            )
        if (
            absence_capable is False
            and _is_integer(applicable_count)
            and applicable_count > 0
            and _is_integer(detected_count)
            and detected_count == 0
        ):
            required_classification = (
                "not_yet_observed"
                if lane == "pattern_classifications"
                else "unclassified"
            )
            if (
                classification != required_classification
                or observation_status != "not_yet_observed"
            ):
                errors.append(
                    f"{path} positive-only zero detection must be "
                    f"{required_classification}/not_yet_observed"
                )
    if observed_ids != expected_ids:
        errors.append(
            f"pattern_profile.{lane} must be sorted and exhaustive for its raw lane; "
            f"expected={expected_ids}, got={observed_ids}"
        )
    return canonical, errors


def _validate_combinations(
    value: object,
    positive_rows: Sequence[Mapping[str, object]],
    *,
    eligible_talk_count: int,
) -> list[str]:
    if not isinstance(value, list):
        return ["pattern_profile.signature_combinations must be an array"]
    errors: list[str] = []
    eligible = {
        row.get("pattern_id")
        for row in positive_rows
        if row.get("classification") in {"regular", "signature"}
    }
    ids: list[str] = []
    previous_sort_key: tuple[float, int, tuple[str, ...]] | None = None
    for index, item in enumerate(value):
        path = f"pattern_profile.signature_combinations[{index}]"
        if not isinstance(item, Mapping):
            errors.append(f"{path} must be an object")
            continue
        errors.extend(_exact_fields(item, _COMBINATION_FIELDS, path=path))
        members = item.get("pattern_ids")
        if not isinstance(members, list) or len(members) not in {2, 3}:
            errors.append(f"{path}.pattern_ids must contain exactly two or three IDs")
            continue
        errors.extend(
            _string_array(members, path=f"{path}.pattern_ids", sorted_required=True)
        )
        combination_id = item.get("combination_id")
        expected_id = "+".join(str(member) for member in members)
        if combination_id != expected_id:
            errors.append(f"{path}.combination_id must equal {expected_id!r}")
        elif isinstance(combination_id, str):
            ids.append(combination_id)
        if any(member not in eligible for member in members):
            errors.append(f"{path} members must each be regular or signature")
        evidence = item.get("evidence")
        if not isinstance(evidence, Mapping):
            errors.append(f"{path}.evidence must be an object")
            continue
        evidence_errors = _validate_evidence(evidence, path=f"{path}.evidence")
        errors.extend(evidence_errors)
        lower = evidence.get("lower")
        detected = evidence.get("detected_count")
        applicable = evidence.get("applicable_count")
        if evidence_errors:
            continue
        assert _is_integer(applicable)
        if applicable > eligible_talk_count:
            errors.append(
                f"{path}.evidence.applicable_count cannot exceed the eligible "
                f"talk count {eligible_talk_count}"
            )
        if not isinstance(lower, (int, float)) or isinstance(lower, bool):
            errors.append(f"{path}.evidence.lower must be numeric")
            continue
        assert _is_integer(detected)
        sort_key = (-float(lower), -detected, tuple(str(member) for member in members))
        if previous_sort_key is not None and sort_key < previous_sort_key:
            errors.append(
                "pattern_profile.signature_combinations must use canonical ranking"
            )
        previous_sort_key = sort_key
        errors.extend(
            _string_array(item.get("reason_codes"), path=f"{path}.reason_codes")
        )
        if item.get("reason_codes") == []:
            errors.append(f"{path}.reason_codes must not be empty")
    if len(ids) != len(set(ids)):
        errors.append(
            "pattern_profile.signature_combinations duplicates a canonical ID"
        )
    if len(value) > 10:
        errors.append(
            "pattern_profile.signature_combinations may contain at most 10 rows"
        )
    return errors


def _validate_trend_metric(
    value: object,
    *,
    path: str,
    available: bool,
    allowed_statuses: frozenset[str],
) -> tuple[object, list[str]]:
    if not isinstance(value, Mapping):
        return None, [f"{path} must be an object"]
    errors = _exact_fields(value, _TREND_METRIC_FIELDS, path=path)
    status = value.get("status")
    if not available:
        expected = {
            "status": "unavailable",
            "prior_average": None,
            "recent_average": None,
            "delta": None,
        }
        if dict(value) != expected:
            errors.append(f"{path} must use the unavailable null sentinel")
        return status, errors

    if status not in allowed_statuses:
        errors.append(f"{path}.status is invalid: {status!r}")
    numbers: dict[str, int | float] = {}
    for field in ("prior_average", "recent_average", "delta"):
        raw = value.get(field)
        if not _is_finite_number(raw):
            errors.append(f"{path}.{field} must be a finite number")
        else:
            numbers[field] = raw
    if len(numbers) == 3:
        try:
            delta_matches = math.isclose(
                float(numbers["delta"]),
                float(numbers["recent_average"]) - float(numbers["prior_average"]),
                rel_tol=0.0,
                abs_tol=1e-12,
            )
        except OverflowError:
            delta_matches = False
        if not delta_matches:
            errors.append(f"{path}.delta must equal recent_average - prior_average")
    return status, errors


def _validate_trend_movement_lane(
    value: object,
    *,
    path: str,
    expected_ids: list[str],
    window_size: int | None,
) -> tuple[list[Mapping[str, object]], list[str]]:
    if not isinstance(value, list):
        return [], [f"{path} must be an array"]
    rows: list[Mapping[str, object]] = []
    observed_ids: list[str] = []
    errors: list[str] = []
    for index, item in enumerate(value):
        row_path = f"{path}[{index}]"
        if not isinstance(item, Mapping):
            errors.append(f"{row_path} must be an object")
            continue
        errors.extend(_exact_fields(item, _TREND_MOVEMENT_FIELDS, path=row_path))
        pattern_id = item.get("pattern_id")
        if not isinstance(pattern_id, str) or not pattern_id:
            errors.append(f"{row_path}.pattern_id must be a non-empty string")
        else:
            observed_ids.append(pattern_id)
            rows.append(item)

        movement = item.get("movement")
        reason_codes = item.get("reason_codes")
        errors.extend(_string_array(reason_codes, path=f"{row_path}.reason_codes"))
        normalized_reasons = (
            tuple(reason_codes)
            if isinstance(reason_codes, list)
            and all(isinstance(reason, str) for reason in reason_codes)
            else ()
        )
        if movement == "unavailable":
            if (
                len(normalized_reasons) != 1
                or normalized_reasons[0] not in _UNAVAILABLE_MOVEMENT_REASON_CODES
            ):
                errors.append(
                    f"{row_path}.reason_codes must explain unavailable movement"
                )
        elif movement in _MOVEMENT_REASON_CODES:
            if normalized_reasons != _MOVEMENT_REASON_CODES[movement]:
                errors.append(
                    f"{row_path}.reason_codes must match movement {movement!r}"
                )
        else:
            errors.append(f"{row_path}.movement is invalid: {movement!r}")

        evidence_by_window: dict[str, Mapping[object, object]] = {}
        evidence_errors_by_window: dict[str, list[str]] = {}
        for field in ("prior_evidence", "recent_evidence"):
            evidence = item.get(field)
            evidence_path = f"{row_path}.{field}"
            if not isinstance(evidence, Mapping):
                errors.append(f"{evidence_path} must be an object")
                continue
            evidence_errors = _validate_evidence(evidence, path=evidence_path)
            errors.extend(evidence_errors)
            evidence_by_window[field] = evidence
            evidence_errors_by_window[field] = evidence_errors

        if window_size is not None and len(evidence_by_window) == 2:
            applicable_counts = [
                evidence_by_window[field].get("applicable_count")
                for field in ("prior_evidence", "recent_evidence")
            ]
            for field, applicable in zip(
                ("prior_evidence", "recent_evidence"),
                applicable_counts,
                strict=True,
            ):
                if _is_integer(applicable) and applicable > window_size:
                    errors.append(
                        f"{row_path}.{field}.applicable_count cannot exceed "
                        f"trend window size {window_size}"
                    )
            complete_windows = all(
                _is_integer(applicable) and applicable == window_size
                for applicable in applicable_counts
            )
            if movement == "unavailable":
                reason = normalized_reasons[0] if len(normalized_reasons) == 1 else None
                if reason == "incomplete_window_applicability" and complete_windows:
                    errors.append(
                        f"{row_path}.movement cannot report incomplete applicability "
                        "for complete windows"
                    )
                if reason == "window_bounds_unavailable" and not any(
                    evidence_errors_by_window.values()
                ):
                    errors.append(
                        f"{row_path}.movement cannot report unavailable bounds for "
                        "valid evidence"
                    )
            elif movement in _MOVEMENT_REASON_CODES and not complete_windows:
                errors.append(
                    f"{row_path}.movement must be unavailable unless both windows "
                    f"have {window_size} applicable talks"
                )

    if observed_ids != expected_ids:
        errors.append(
            f"{path} must be sorted and exhaustive; "
            f"expected={expected_ids}, got={observed_ids}"
        )
    return rows, errors


def _validate_trend_analysis(
    pattern_profile: Mapping[str, object],
    positive_rows: Sequence[Mapping[str, object]],
    antipattern_rows: Sequence[Mapping[str, object]],
    *,
    eligible_talk_count: int,
    required_talk_count: int | None,
    window_size: int | None,
) -> list[str]:
    value = pattern_profile.get("trend_analysis")
    if not isinstance(value, Mapping):
        return ["pattern_profile.trend_analysis must be an object"]
    path = "pattern_profile.trend_analysis"
    errors = _exact_fields(value, _TREND_ANALYSIS_FIELDS, path=path)
    status = value.get("status")
    reason_codes = value.get("reason_codes")
    errors.extend(_string_array(reason_codes, path=f"{path}.reason_codes"))
    reasons = (
        list(reason_codes)
        if isinstance(reason_codes, list)
        and all(isinstance(reason, str) for reason in reason_codes)
        else []
    )
    available = status == "available"
    unavailable_reason: str | None = None
    if available:
        if reasons != []:
            errors.append(f"{path}.reason_codes must be [] when available")
    elif status == "unavailable":
        if len(reasons) != 1 or reasons[0] not in _TREND_UNAVAILABLE_REASONS:
            errors.append(
                f"{path}.reason_codes must contain one supported unavailability reason"
            )
        else:
            unavailable_reason = reasons[0]
    else:
        errors.append(f"{path}.status must be available or unavailable")

    availability = pattern_profile.get("classification_availability")
    trend_availability = (
        availability.get("trends") if isinstance(availability, Mapping) else None
    )
    expected_availability = {"status": status, "reason_codes": reasons}
    if (
        not isinstance(trend_availability, Mapping)
        or dict(trend_availability) != expected_availability
    ):
        errors.append(
            "pattern_profile.classification_availability.trends must exactly mirror "
            "trend_analysis status and reasons"
        )

    baseline_value = pattern_profile.get("baseline_talk_filenames")
    baseline_filenames = (
        list(baseline_value)
        if isinstance(baseline_value, list)
        and all(isinstance(filename, str) for filename in baseline_value)
        else []
    )
    baseline_set = set(baseline_filenames)
    sample = value.get("sample")
    sample_required: int | None = None
    valid_date_count: int | None = None
    invalid_filenames: list[str] | None = None
    selected_filenames: list[str] | None = None
    identity: object = None
    if not isinstance(sample, Mapping):
        errors.append(f"{path}.sample must be an object")
    else:
        sample_path = f"{path}.sample"
        errors.extend(_exact_fields(sample, _TREND_SAMPLE_FIELDS, path=sample_path))
        raw_required = sample.get("required_talk_count")
        if not _is_integer(raw_required) or raw_required < 1:
            errors.append(
                f"{sample_path}.required_talk_count must be a positive integer"
            )
        else:
            sample_required = raw_required
            if (
                required_talk_count is not None
                and sample_required != required_talk_count
            ):
                errors.append(
                    f"{sample_path}.required_talk_count must equal the applied policy "
                    f"value {required_talk_count}"
                )
        raw_valid = sample.get("valid_date_talk_count")
        if not _is_integer(raw_valid) or raw_valid < 0:
            errors.append(
                f"{sample_path}.valid_date_talk_count must be a non-negative integer"
            )
        else:
            valid_date_count = raw_valid

        raw_invalid = sample.get("invalid_date_filenames")
        invalid_errors = _string_array(
            raw_invalid,
            path=f"{sample_path}.invalid_date_filenames",
            sorted_required=True,
        )
        errors.extend(invalid_errors)
        if not invalid_errors and isinstance(raw_invalid, list):
            invalid_filenames = list(raw_invalid)
        raw_selected = sample.get("selected_filenames")
        selected_errors = _string_array(
            raw_selected, path=f"{sample_path}.selected_filenames"
        )
        errors.extend(selected_errors)
        if not selected_errors and isinstance(raw_selected, list):
            selected_filenames = list(raw_selected)

        for field, filenames in (
            ("invalid_date_filenames", invalid_filenames),
            ("selected_filenames", selected_filenames),
        ):
            if filenames is not None:
                unknown = sorted(set(filenames) - baseline_set)
                if unknown:
                    errors.append(
                        f"{sample_path}.{field} contains filenames outside the "
                        f"baseline: {unknown}"
                    )
        if (
            invalid_filenames is not None
            and selected_filenames is not None
            and set(invalid_filenames).intersection(selected_filenames)
        ):
            errors.append(
                f"{sample_path}.selected_filenames cannot include invalid-date talks"
            )
        if valid_date_count is not None and invalid_filenames is not None:
            if valid_date_count + len(invalid_filenames) != eligible_talk_count:
                errors.append(
                    f"{sample_path} valid and invalid date counts must equal the "
                    f"eligible talk count {eligible_talk_count}"
                )
        identity = sample.get("opportunity_coverage_identity")

    if available:
        if (
            sample_required is not None
            and selected_filenames is not None
            and len(selected_filenames) != sample_required
        ):
            errors.append(
                f"{path}.sample.selected_filenames must contain exactly "
                f"{sample_required} talks when trends are available"
            )
        if valid_date_count is not None and sample_required is not None:
            if valid_date_count < sample_required:
                errors.append(
                    f"{path}.sample does not have enough valid dated talks for trends"
                )
        if not _is_lower_hex64(identity):
            errors.append(
                f"{path}.sample.opportunity_coverage_identity must be lowercase hex64 "
                "when trends are available"
            )
    elif unavailable_reason == "insufficient_valid_date_sample":
        if valid_date_count is not None and sample_required is not None:
            if valid_date_count >= sample_required:
                errors.append(
                    f"{path}.sample must have fewer valid talks than required for "
                    "insufficient_valid_date_sample"
                )
        if selected_filenames not in (None, []):
            errors.append(
                f"{path}.sample.selected_filenames must be [] for an insufficient sample"
            )
        if identity is not None:
            errors.append(
                f"{path}.sample.opportunity_coverage_identity must be null for an "
                "insufficient sample"
            )
    elif unavailable_reason in {
        "opportunity_identity_unavailable",
        "incomparable_opportunity_identities",
        "no_evaluable_pattern_opportunities",
    }:
        if (
            sample_required is not None
            and selected_filenames is not None
            and len(selected_filenames) != sample_required
        ):
            errors.append(
                f"{path}.sample.selected_filenames must contain exactly "
                f"{sample_required} talks after sample selection"
            )
        if valid_date_count is not None and sample_required is not None:
            if valid_date_count < sample_required:
                errors.append(f"{path}.sample must have enough valid dated talks")
        if unavailable_reason == "no_evaluable_pattern_opportunities":
            if not _is_lower_hex64(identity):
                errors.append(
                    f"{path}.sample.opportunity_coverage_identity must be lowercase "
                    "hex64 after identity resolution"
                )
        elif identity is not None:
            errors.append(
                f"{path}.sample.opportunity_coverage_identity must be null while "
                "identity resolution is unavailable"
            )

    score_status, metric_errors = _validate_trend_metric(
        value.get("score"),
        path=f"{path}.score",
        available=available,
        allowed_statuses=frozenset({"improving", "declining", "stable"}),
    )
    errors.extend(metric_errors)
    breadth_status, metric_errors = _validate_trend_metric(
        value.get("breadth"),
        path=f"{path}.breadth",
        available=available,
        allowed_statuses=frozenset({"widening", "narrowing", "stable"}),
    )
    errors.extend(metric_errors)

    pattern_rows: list[Mapping[str, object]] = []
    antipattern_movement_rows: list[Mapping[str, object]] = []
    if available:
        pattern_rows, movement_errors = _validate_trend_movement_lane(
            value.get("pattern_movements"),
            path=f"{path}.pattern_movements",
            expected_ids=[
                str(row["pattern_id"])
                for row in positive_rows
                if isinstance(row.get("pattern_id"), str)
            ],
            window_size=window_size,
        )
        errors.extend(movement_errors)
        antipattern_movement_rows, movement_errors = _validate_trend_movement_lane(
            value.get("antipattern_movements"),
            path=f"{path}.antipattern_movements",
            expected_ids=[
                str(row["pattern_id"])
                for row in antipattern_rows
                if isinstance(row.get("pattern_id"), str)
            ],
            window_size=window_size,
        )
        errors.extend(movement_errors)
    else:
        for field in ("pattern_movements", "antipattern_movements"):
            if value.get(field) != []:
                errors.append(f"{path}.{field} must be [] when trends are unavailable")

    if pattern_profile.get("score_trend") != score_status:
        errors.append(
            "pattern_profile.score_trend must project trend_analysis.score.status"
        )
    profile_breadth = pattern_profile.get("pattern_breadth")
    if (
        not isinstance(profile_breadth, Mapping)
        or profile_breadth.get("trend") != breadth_status
    ):
        errors.append(
            "pattern_profile.pattern_breadth.trend must project "
            "trend_analysis.breadth.status"
        )

    expected_pattern_drivers = sorted(
        str(row["pattern_id"])
        for row in pattern_rows
        if row.get("movement") in {"increasing", "decreasing"}
        and isinstance(row.get("pattern_id"), str)
    )
    expected_antipattern_drivers = sorted(
        str(row["pattern_id"])
        for row in antipattern_movement_rows
        if row.get("movement") in {"increasing", "decreasing"}
        and isinstance(row.get("pattern_id"), str)
    )
    score_drivers = pattern_profile.get("score_drivers")
    if not isinstance(score_drivers, Mapping):
        errors.append("pattern_profile.score_drivers must be an object")
    else:
        if score_drivers.get("direction") != score_status:
            errors.append(
                "pattern_profile.score_drivers.direction must project "
                "trend_analysis.score.status"
            )
        if score_drivers.get("pattern_drivers") != expected_pattern_drivers:
            errors.append(
                "pattern_profile.score_drivers.pattern_drivers must project "
                "pattern trend movements"
            )
        if score_drivers.get("antipattern_drivers") != expected_antipattern_drivers:
            errors.append(
                "pattern_profile.score_drivers.antipattern_drivers must project "
                "antipattern trend movements"
            )
    return errors


def _validate_v5_policy_fields(
    pattern_profile: Mapping[str, object],
    active_catalog: object,
    *,
    eligible_talk_count: int,
) -> tuple[frozenset[str], str | None, list[str]]:
    errors: list[str] = []
    required_talk_count: int | None = None
    window_size: int | None = None
    classification_schema_version = pattern_profile.get("classification_schema_version")
    if (
        not _is_integer(classification_schema_version)
        or classification_schema_version != CLASSIFICATION_SCHEMA_VERSION
    ):
        errors.append("pattern_profile.classification_schema_version must be 1")
    try:
        stamp = validate_policy_stamp(pattern_profile.get("classification_policy"))
        digest = str(stamp["semantic_sha256"])
        semantic_policy = stamp.get("semantic_policy")
        trend_policy = (
            semantic_policy.get("trends")
            if isinstance(semantic_policy, Mapping)
            else None
        )
        if isinstance(trend_policy, Mapping):
            raw_required = trend_policy.get("minimum_comparable_talks")
            raw_window = trend_policy.get("window_size")
            if _is_integer(raw_required):
                required_talk_count = raw_required
            if _is_integer(raw_window):
                window_size = raw_window
    except (RuntimeError, ValueError) as exc:
        errors.append(f"pattern_profile.classification_policy is invalid: {exc}")
        digest = None
    domains, availability_errors = _validate_availability_v2(
        pattern_profile.get("classification_availability")
    )
    errors.extend(availability_errors)
    positive_rows, lane_errors = _validate_classification_lane(
        pattern_profile.get("pattern_classifications"),
        pattern_profile.get("pattern_usage"),
        lane="pattern_classifications",
        allowed_classifications=_POSITIVE_CLASSIFICATIONS,
        active_catalog=active_catalog,
    )
    errors.extend(lane_errors)
    antipattern_rows, lane_errors = _validate_classification_lane(
        pattern_profile.get("antipattern_classifications"),
        pattern_profile.get("antipattern_frequency"),
        lane="antipattern_classifications",
        allowed_classifications=_ANTIPATTERN_CLASSIFICATIONS,
        active_catalog=active_catalog,
    )
    errors.extend(lane_errors)

    expected_mastery = {
        tier: [
            str(row["pattern_id"])
            for row in positive_rows
            if row.get("classification") == tier
        ]
        for tier in _MASTERY_TIERS
    }
    if pattern_profile.get("mastery_levels") != expected_mastery:
        errors.append(
            "pattern_profile.mastery_levels is not the deterministic projection"
        )
    never_tried = expected_mastery["never_tried"]
    if pattern_profile.get("never_used_patterns") != never_tried:
        errors.append(
            "pattern_profile.never_used_patterns must equal confirmed never_tried IDs"
        )
    underused = sorted(expected_mastery["rare"] + never_tried)
    if pattern_profile.get("underused_patterns") != underused:
        errors.append(
            "pattern_profile.underused_patterns must equal sorted rare + never_tried IDs"
        )
    strengths = sorted(expected_mastery["regular"] + expected_mastery["signature"])
    if pattern_profile.get("strengths") != strengths:
        errors.append(
            "pattern_profile.strengths must equal sorted regular + signature IDs"
        )
    errors.extend(
        _validate_combinations(
            pattern_profile.get("signature_combinations"),
            positive_rows,
            eligible_talk_count=eligible_talk_count,
        )
    )
    if pattern_profile.get("by_mode") != []:
        errors.append(
            "pattern_profile.by_mode must be [] while talk mode assignments are unavailable"
        )

    errors.extend(
        _validate_trend_analysis(
            pattern_profile,
            positive_rows,
            antipattern_rows,
            eligible_talk_count=eligible_talk_count,
            required_talk_count=required_talk_count,
            window_size=window_size,
        )
    )
    if "antipattern_recurrence" not in domains:
        actionable = {
            row.get("classification")
            for row in antipattern_rows
            if row.get("classification") in {"high_frequency", "moderate_frequency"}
        }
        if actionable:
            errors.append(
                "unavailable antipattern recurrence cannot expose actionable tiers"
            )
    return domains, digest, errors


def assess_pattern_profile(
    pattern_profile: object,
    *,
    expected_contract_version: int | None = None,
) -> PatternProfileAssessment:
    """Assess v4 occurrence-only or v5 policy-bound pattern history."""
    errors: list[str] = []
    reason_codes: list[str] = []
    if not isinstance(pattern_profile, Mapping):
        return PatternProfileAssessment(
            current_contract=False,
            catalog_fields_available=False,
            scored_talk_count=None,
            reason_codes=(REASON_INVALID_CONTRACT,),
            errors=("pattern_profile must be an object",),
            contract_version=expected_contract_version,
        )

    inferred_version = (
        5
        if any(
            field in pattern_profile
            for field in _V5_PATTERN_PROFILE_FIELDS - _V4_PATTERN_PROFILE_FIELDS
        )
        else 4
    )
    contract_version = expected_contract_version or inferred_version
    if contract_version not in {4, 5}:
        return PatternProfileAssessment(
            current_contract=False,
            catalog_fields_available=False,
            scored_talk_count=None,
            reason_codes=(REASON_INVALID_CONTRACT,),
            errors=(
                f"unsupported pattern-profile contract version {contract_version!r}",
            ),
            contract_version=contract_version,
        )
    required_fields = (
        _V5_PATTERN_PROFILE_FIELDS
        if contract_version == 5
        else _V4_PATTERN_PROFILE_FIELDS
    )
    missing_fields = sorted(required_fields - set(pattern_profile))
    unknown_fields = sorted(set(pattern_profile) - required_fields, key=str)
    if missing_fields:
        errors.append(
            f"pattern_profile is missing required schema-v{contract_version} fields: "
            f"{', '.join(missing_fields)}"
        )
    if unknown_fields:
        errors.append(
            f"pattern_profile has unknown schema-v{contract_version} fields: "
            f"{', '.join(str(field) for field in unknown_fields)}"
        )
    errors.extend(_validate_common_shape(pattern_profile))

    try:
        baseline = validate_adherence_baseline(pattern_profile.get("pattern_baseline"))
    except AdherenceBaselineError as exc:
        errors.append(f"pattern_profile.pattern_baseline is invalid: {exc}")
        _append_reason(reason_codes, REASON_INVALID_CONTRACT)
        return PatternProfileAssessment(
            current_contract=False,
            catalog_fields_available=False,
            scored_talk_count=None,
            reason_codes=tuple(reason_codes),
            errors=tuple(errors),
            contract_version=contract_version,
        )

    score_comparable_count = baseline["scored_talk_count"]
    assert isinstance(score_comparable_count, int)
    raw_eligible_count = baseline.get("eligible_talk_count")
    if not _is_integer(raw_eligible_count) or raw_eligible_count < 0:
        errors.append(
            "pattern_profile.pattern_baseline must use scoring-v5 baseline schema 2 "
            "with a non-negative eligible_talk_count"
        )
        eligible_count = 0
    else:
        eligible_count = raw_eligible_count
    if baseline["active_batch_excluded"] is not False:
        errors.append(
            "pattern_profile.pattern_baseline must be a full-cohort snapshot with "
            "active_batch_excluded=false"
        )
    if baseline["excluded_filenames"] != []:
        errors.append("pattern_profile.pattern_baseline.excluded_filenames must be []")

    active_catalog = None
    try:
        active_catalog = load_catalog()
        active_fingerprint = active_catalog.fingerprint
    except ReturnValidationError as exc:
        errors.append(
            "could not resolve the active Presentation Pattern catalog; repair the "
            f"installed catalog before using pattern history: {exc}"
        )
        _append_reason(reason_codes, REASON_ACTIVE_CATALOG_UNAVAILABLE)
        active_fingerprint = None
    if (
        active_fingerprint is not None
        and baseline["pattern_catalog_fingerprint"] != active_fingerprint
    ):
        errors.append(
            "pattern_profile.pattern_baseline.pattern_catalog_fingerprint does not "
            "match the active catalog; regenerate the entire pattern profile as a "
            "new scoring generation"
        )
        _append_reason(reason_codes, REASON_CATALOG_FINGERPRINT_MISMATCH)
    if baseline["pattern_scoring_schema_version"] != PATTERN_SCORING_SCHEMA_VERSION:
        errors.append(
            "pattern_profile.pattern_baseline.pattern_scoring_schema_version is "
            f"{baseline['pattern_scoring_schema_version']!r}; expected active schema "
            f"{PATTERN_SCORING_SCHEMA_VERSION}"
        )
        _append_reason(reason_codes, REASON_SCORING_SCHEMA_MISMATCH)

    filenames, filename_errors = _canonical_cohort_filenames(
        pattern_profile.get("baseline_talk_filenames")
    )
    errors.extend(filename_errors)
    if filenames is not None and len(filenames) != eligible_count:
        errors.append(
            "pattern_profile.baseline_talk_filenames length must equal "
            f"pattern_baseline.eligible_talk_count {eligible_count}, got {len(filenames)}"
        )
    profile_eligible_count = pattern_profile.get("eligible_talk_count")
    if (
        not _is_integer(profile_eligible_count)
        or profile_eligible_count != eligible_count
    ):
        errors.append(
            "pattern_profile.eligible_talk_count must equal "
            f"pattern_baseline.eligible_talk_count {eligible_count}, got "
            f"{profile_eligible_count!r}"
        )
    talks_scored = pattern_profile.get("talks_scored")
    if not _is_integer(talks_scored) or talks_scored != score_comparable_count:
        errors.append(
            "pattern_profile.talks_scored must equal pattern_baseline.scored_talk_count "
            f"{score_comparable_count}, got {talks_scored!r}"
        )
    profile_average = pattern_profile.get("average_pattern_score")
    baseline_average = baseline["average_pattern_score"]
    if score_comparable_count == 0:
        average_is_valid = profile_average is None
    else:
        average_is_valid = (
            not isinstance(profile_average, bool)
            and isinstance(profile_average, (int, float))
            and math.isfinite(profile_average)
            and profile_average == baseline_average
        )
    if not average_is_valid:
        errors.append(
            "pattern_profile.average_pattern_score must equal the canonical "
            f"pattern baseline average {baseline_average!r}, got {profile_average!r}"
        )
    if active_catalog is not None:
        try:
            errors.extend(
                validate_pattern_opportunity_rows(
                    pattern_profile.get("pattern_usage"),
                    pattern_profile.get("antipattern_frequency"),
                    eligible_cohort_count=eligible_count,
                    catalog=active_catalog,
                )
            )
        except PatternOpportunityError as exc:
            errors.append(f"pattern opportunity rows are invalid: {exc}")

    domains = frozenset()
    policy_digest = None
    if contract_version == 4:
        errors.extend(_validate_v4_unavailable(pattern_profile))
    elif active_catalog is not None:
        domains, policy_digest, policy_errors = _validate_v5_policy_fields(
            pattern_profile,
            active_catalog,
            eligible_talk_count=eligible_count,
        )
        errors.extend(policy_errors)
        if policy_digest is None:
            _append_reason(reason_codes, REASON_CLASSIFICATION_POLICY_INVALID)

    if errors:
        _append_reason(reason_codes, REASON_INVALID_CONTRACT)
        return PatternProfileAssessment(
            current_contract=False,
            catalog_fields_available=False,
            scored_talk_count=score_comparable_count,
            reason_codes=tuple(reason_codes),
            errors=tuple(errors),
            eligible_talk_count=eligible_count,
            available_classification_domains=frozenset(),
            contract_version=contract_version,
            policy_semantic_sha256=policy_digest,
        )

    if eligible_count == 0:
        _append_reason(reason_codes, REASON_EMPTY_CURRENT_COHORT)
    if contract_version == 4:
        _append_reason(reason_codes, REASON_CLASSIFICATION_POLICY_UNAVAILABLE)
    return PatternProfileAssessment(
        current_contract=True,
        catalog_fields_available=eligible_count > 0,
        scored_talk_count=score_comparable_count,
        reason_codes=tuple(reason_codes),
        errors=(),
        eligible_talk_count=eligible_count,
        classification_fields_available=bool(domains),
        available_classification_domains=domains,
        contract_version=contract_version,
        policy_semantic_sha256=policy_digest,
    )

skills

README.md

tile.json