"""FROZEN CONTRACT — topic-agnostic skill birth.

This file is the ONLY seam between the three builders. All three import it; none of them
may edit it. It carries type-annotated dataclasses, enums, the engine's public API as an
abstract surface, the F001 accounting-ledger schema, typed fail-loud errors, and the pinned
node_core baseline shas (G11). No behavior lives here beyond trivial constructors.

Design invariant (the whole point): the birth engine is TOPIC-AGNOSTIC. It may read a
fixture's data and I/O SHAPE; it may call the fixture's checker; it may NOT read a fixture's
topic identity. Concretely, the engine (skill_birth.py) MUST NOT read `TaskFamily.name` or
`TaskFamily.expect` — those two fields exist for the harness and the ledger ONLY. The G1
mechanical scan enforces this by rejecting any `.name`/`.expect` access and any topic-word
branch in skill_birth.py. Topic-specific facts live only in fixtures (examples, checkers,
negatives, seeds) and in born skills after adoption.

stdlib-only. Python 3.10+.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple


# ─────────────────────────────────────────────────────────────────────────────────────────
# G11 — pinned node_core baseline. The harness (Builder C) recomputes these at run start and
# asserts equality; skill_birth.py must never write into node_core/. Captured 2026-07-05.
# ─────────────────────────────────────────────────────────────────────────────────────────
NODE_CORE_SHAS: Dict[str, str] = {
    "node_core/engine.py": "7efaf4ac36129bbe2a530b3a2ef8c09ab03577f67d30ab646a2b17f58e00c99e",
    "node_core/loop.py": "5e00305dc115bccfa8917e2b3ff48457be7af3bb9f7f570ab2b88d015df170c0",
    "node_core/skill_abi.py": "6c5d48c6098ca5d6ed401824fd68c49a897c56a07f1e33ae4dcb9308638d1b5c",
    "node_core/__init__.py": "17ef15ec63560bc4e8ea9301188312979268181c54f91c12cc4b9c44bd63877e",
    "node_core/selftest.py": "247cd8ab0bf889c27b02c0be47191ea3fdb8bd47495924c4df1772d316c43412",
}

# The generic operator algebra. The ONLY operators the engine may propose. Each is
# content-free; its parameters are enumerated by inspecting example I/O SHAPE, never a topic
# label. `compose`/`sequence` are the compounding hooks (they draw atoms from adopted skills).
GENERIC_OPERATORS: Tuple[str, ...] = (
    "map", "filter", "normalize", "extract", "classify",
    "compose", "compare", "lookup", "reduce", "sequence",
    # integration extension (2026-07-05, lead): the algebra had NO record->record
    # constructor, so any record-in/record-out family hit a reach limit by construction.
    # `fieldmap` = one observed key's str value through a normalize scheme, identity on the
    # rest — parameters from observed shape/keys only, content-free like the rest.
    "fieldmap",
)


# ─────────────────────────────────────────────────────────────────────────────────────────
# Typed, fail-loud errors. No silent drops (AGENT.md §7).
# ─────────────────────────────────────────────────────────────────────────────────────────
class SkillBirthError(Exception):
    """Base for every failure in this experiment."""


class MalformedFamilyError(SkillBirthError):
    """A TaskFamily is missing required data or a checker; raised loudly, never swallowed."""


class TopicLeakError(SkillBirthError):
    """Defensive: raised if engine code ever reaches a fixture's topic identity. Its existence
    documents the contract; the real guard is the G1 mechanical scan."""


class NonDeterminismError(SkillBirthError):
    """Raised by the harness when two runs disagree (G10) or an enumeration leaked set order."""


# ─────────────────────────────────────────────────────────────────────────────────────────
# Enums
# ─────────────────────────────────────────────────────────────────────────────────────────
class BirthStatus(str, Enum):
    ADOPTED = "adopted"
    REFUSED = "refused"


class RefusalKind(str, Enum):
    REACH_LIMIT = "reach_limit"          # 0 candidates fit train — need an operation I don't have
    UNDERDETERMINED = "underdetermined"  # >1 distinct behavior; a discriminating input exists
    INDISTINGUISHABLE = "indistinguishable"  # >1 behavior but NO input in the pool separates them
    CONTRADICTION = "contradiction"      # examples are mutually inconsistent — refuse, don't pick


class ExpectTag(str, Enum):
    """Harness-only expected outcome for a fixture (drives README bars). NEVER read by the engine."""
    ADOPT = "adopt"
    REUSE = "reuse"    # adopt, AND the survivor must depend on a previously born skill
    REFUSE = "refuse"


# ─────────────────────────────────────────────────────────────────────────────────────────
# Family fixtures (Builder B owns the instances; this is their frozen shape)
# ─────────────────────────────────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class NegativeRef:
    """A plausible-but-WRONG rule the family supplies. A correct skill must NOT reproduce this
    behavior on the probe pool. Kills coincidental train-fit (S036 unique-determination hardened)."""
    label: str
    program: Callable[[Any], Any]  # input -> a wrong answer (topic-specific; lives in the fixture)


@dataclass(frozen=True)
class TaskFamily:
    """One unknown-task family. Builder B fills these with topic-specific data ONLY; no engine
    logic. The engine reads `examples/holdout/adversarial_seeds/negatives/probe_pool/checker/
    available_atoms` and calls `checker`; it MUST NOT read `name` or `expect`.

    examples          : [(input, answer)] REAL worked examples. Candidates are PROPOSED from these ONLY.
    holdout           : [(input, answer)] disjoint from examples; used to VALIDATE + DISCRIMINATE,
                        never to propose (G5). Must be discriminating (informative regime, S036).
    adversarial_seeds : [input] hard inputs the generic mutator can't invent (data only; the engine
                        still decides generically, running each through `checker`).
    negatives         : wrong-rule references (see NegativeRef).
    probe_pool        : [input] candidate discriminating inputs for the S047 refusal request. For a
                        REFUSE family this must contain an input where the surviving rules disagree.
    checker           : (input, answer) -> bool — the family's OWN oracle (F003: never a universal
                        verifier). The engine calls it; it does not branch on the family.
    available_atoms   : names of generic atoms the family declares present (subset of GENERIC_OPERATORS
                        params). Optional hint; data, not a branch.
    name / expect     : HARNESS + LEDGER ONLY. Off-limits to the engine.
    """
    name: str
    examples: List[Tuple[Any, Any]]
    holdout: List[Tuple[Any, Any]]
    adversarial_seeds: List[Any]
    negatives: List[NegativeRef]
    probe_pool: List[Any]
    checker: Callable[[Any, Any], bool]
    expect: ExpectTag
    available_atoms: Tuple[str, ...] = ()


# ─────────────────────────────────────────────────────────────────────────────────────────
# Engine intermediate artifacts
# ─────────────────────────────────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class SkillHypothesis:
    """Step 3 — the engine NAMES the missing skill deterministically from observed shape + a
    stable hash. `skill_name` carries NO topic word (arity/shape tags + hex8 hash only)."""
    skill_name: str
    in_shape: str          # structural tag, e.g. "seq[text]", "record<n=3>", "text", "scalar"
    out_shape: str
    n_train: int
    derived_from: str      # human-readable note on how the name was derived (provenance)


@dataclass(frozen=True)
class EmpiricalStandard:
    """Step 4-5 — the plain-English success standard AND its machine-checkable decomposition,
    RECORDED BEFORE any adoption (G3). Deterministic templating over observed counts/shape
    (S047 precedent: derived templating over I/O shape counts as plain English; no model)."""
    skill_name: str
    plain_english: str
    success_rule: str                 # the one-line adopt-iff rule
    n_train: int
    n_holdout: int
    n_adversarial: int
    n_negative: int
    contradiction_check: str          # description of the injected-inconsistency soundness test
    refusal_conditions: Tuple[str, ...]


@dataclass(frozen=True)
class CandidateSpec:
    """A proposed skill behavior: a generic operator + shape-derived params + prerequisite deps.
    JSON-able and logic-free; the engine rebuilds the program from this spec. `deps` non-empty
    ⇒ this candidate reuses already-born skills (the compounding edge, G7)."""
    op: str                    # one of GENERIC_OPERATORS
    params: Dict[str, Any]     # shape-derived, JSON-able (observed keys/indices/labels/schemes)
    deps: Tuple[str, ...] = ()


@dataclass(frozen=True)
class BornSkill:
    """An adopted skill. Becomes an atom for later births (registry-as-operator-source)."""
    name: str
    spec: CandidateSpec
    deps: Tuple[str, ...]
    in_shape: str
    out_shape: str
    provenance: str


@dataclass(frozen=True)
class RefusalRequest:
    """Step 9 — the constructive refusal (S047). For UNDERDETERMINED, `missing_input` is the EXACT
    single input to label and `disagreeing_values` are what the surviving rules give there."""
    kind: RefusalKind
    plain_english: str
    candidates: Tuple[str, ...] = ()
    missing_input: Optional[Any] = None
    disagreeing_values: Tuple[Any, ...] = ()


@dataclass(frozen=True)
class BirthCounts:
    """F001 honest accounting for ONE family — EVERY unit of work, no hidden sweeps. `proposals`
    is the FULL enumerated candidate count (not survivors). Check counts include candidates that
    failed. `reuse_events` counts adopted-skill atoms consumed by the survivor's spec."""
    proposals: int = 0
    train_checks: int = 0
    holdout_checks: int = 0
    adversarial_checks: int = 0
    negative_checks: int = 0
    contradiction_checks: int = 0
    survivors: int = 0
    adoptions: int = 0        # 0 or 1
    refusals: int = 0         # 0 or 1
    reuse_events: int = 0


@dataclass(frozen=True)
class BirthOutcome:
    """The result of birthing against ONE family (steps 2-9)."""
    family: str                        # reporting label (from TaskFamily.name — set by the ENGINE
                                       # ONLY at return time from a value the harness passes in
                                       # separately; see SkillBirthEngine.birth docstring)
    status: BirthStatus
    hypothesis: SkillHypothesis
    standard: EmpiricalStandard
    detected_unknown: bool             # G2 — was this a genuine registry miss?
    adopted: Optional[BornSkill]
    refusal: Optional[RefusalRequest]
    reused_deps: Tuple[str, ...]
    counts: BirthCounts


@dataclass
class AccountingLedger:
    """F001 whole-run ledger. Mutable so the engine appends per family; serialized deterministically
    (sort every collection before dump) for G10 byte-identity."""
    per_family: List[BirthCounts] = field(default_factory=list)
    skills_adopted: List[str] = field(default_factory=list)
    families_with_adoption: List[str] = field(default_factory=list)
    refusals: List[str] = field(default_factory=list)          # family labels that refused
    reuse_edges: List[Tuple[str, str]] = field(default_factory=list)  # (born_skill, reusing_family)

    def totals(self) -> BirthCounts:
        """Sum of per-family counts. Trivial reducer (allowed here)."""
        t = BirthCounts()
        acc = {k: 0 for k in vars(t)}
        for c in self.per_family:
            for k, v in vars(c).items():
                acc[k] += v
        return BirthCounts(**acc)


# ─────────────────────────────────────────────────────────────────────────────────────────
# The engine's PUBLIC API (Builder A implements; Builder C calls). Signatures are frozen.
# ─────────────────────────────────────────────────────────────────────────────────────────
class SkillBirthEngine:
    """Topic-agnostic skill-birth loop. One universal decision path over ALL families.

    Contract for Builder A:
      * Read a family's data/shape and call its checker. NEVER read `family.name`/`family.expect`.
        (`birth` receives the reporting label as a SEPARATE `label` argument so the engine body
        never touches `family.name`.)
      * No RNG, no wall-clock, no set-iteration-order in any returned value (sort before emit).
      * Propose candidates from `examples` ONLY; validate on `holdout`/adversarial/negatives (G5).
      * Every candidate evaluated and every check run is counted into BirthCounts (F001).
      * Adopt iff EXACTLY ONE distinct surviving behavior AND the holdout is discriminating; else
        return a typed RefusalRequest (S047). Fail loud on malformed input.
    """

    def __init__(self) -> None:
        """Empty registry; empty ledger. No arguments (determinism)."""
        raise NotImplementedError

    def registry_names(self) -> List[str]:
        """Sorted names of currently adopted (born) skills."""
        raise NotImplementedError

    def detect_unknown(self, family: TaskFamily) -> bool:
        """G2 — True iff NO adopted skill already reproduces the family's examples (registry miss,
        identified not guessed). Records the miss."""
        raise NotImplementedError

    def name_skill(self, family: TaskFamily) -> SkillHypothesis:
        """Step 3 — deterministic hash/shape-derived name (no topic word, no model)."""
        raise NotImplementedError

    def write_standard(self, hypothesis: SkillHypothesis, family: TaskFamily) -> EmpiricalStandard:
        """Steps 4-5 — plain-English standard + machine-checkable decomposition, before adoption."""
        raise NotImplementedError

    def enumerate_candidates(self, family: TaskFamily) -> List[CandidateSpec]:
        """Step 6 — generic operators over observed shapes + compositions over the registry.
        Deterministically ordered. Its length is the `proposals` count (F001)."""
        raise NotImplementedError

    def birth(self, family: TaskFamily, label: str) -> BirthOutcome:
        """Steps 2-9 for ONE family, in order: detect_unknown → name → write_standard →
        enumerate → evaluate (train → holdout → adversarial → negative → contradiction →
        unique-determination) → adopt-or-refuse. On adopt, updates the registry so later
        families can reuse the born skill (step 10 / G7). `label` is the reporting string
        (the harness passes `family.name`); the engine's DECISION must not depend on it."""
        raise NotImplementedError

    def ledger(self) -> AccountingLedger:
        """The F001 whole-run accounting ledger accumulated across all birth() calls."""
        raise NotImplementedError
