#!/usr/bin/env python3
"""
===============================================================================
HDGL / $620 CHALLENGE HUNTER — v8
===============================================================================

FACTOR-FORM + MULTI-VANTAGE + CUBIC FROBENIUS
RAM-FIRST / LOW-I-O / RESUMABLE LEARNING EDITION

v8 PRINCIPLES
--------------

1. EXACT DOMAIN REDUCTION
   ----------------------
   Search n = p*q where:

       p is prime
       q = a*p + b
       q is prime
       n = p*q
       n mod 5 in {2,3}

   The factorized Fermat condition is tested directly:

       2^(n-1) == 1 (mod p)
       2^(n-1) == 1 (mod q)

   Since p and q are prime and distinct, this is equivalent to:

       2^(n-1) == 1 (mod n)

2. $620 DEFINITION
   ----------------
   A $620 candidate satisfies:

       n is composite
       n mod 5 in {2,3}
       2^(n-1) == 1 mod n
       F_n == 0 mod n

   The final verifier is kept independent from the shortcut pipeline.

3. ALGEBRAIC VANTAGES
   -------------------
   Vantage D=5:
       Fibonacci / discriminant 5 closure.

   Vantage D=8:
       Lucas closure with P=-2, Q=-1.

   Vantage D=13:
       Lucas closure with P=-3, Q=-1.

   Cubic Frobenius:
       f(x) = x^3 - x^2 - x - 1

       x^n == x mod (n, f)

   IMPORTANT:
   These are candidate filters. They are NOT silently promoted to
   logical consequences of the $620 definition.

4. CUBIC FROBENIUS v8
   -------------------
   The previous implementation factored f modulo n and then performed
   a composition check which could become tautological after the gcd
   extraction.

   v8 instead directly checks:

       x^n mod f == x

   This is the actual Frobenius polynomial congruence.

   The polynomial discriminant is:

       disc(f) = -44

   gcd(n,44) is reported diagnostically. It is NOT used as an automatic
   rejection in the normal pipeline.

5. LEARNED GAP-SPIRAL
   -------------------
   The learned accelerator remains HEURISTIC.

   It learns gaps between cubic survivors and applies a decreasing
   skip fraction:

       1
       0.618...
       0.382...

   However, v8 stores the COMPLETE learner state:

       last_hit_p
       cycle_index
       active skip interval
       cooldown
       observations
       useful observations
       skipped count
       arm events
       last gap
       last fraction
       last skip size

   A resume therefore does not reconstruct an incomplete state.

6. NO STORED FLOAT PHI
   --------------------
   The gap fractions are generated from the exact integer Fibonacci
   recurrence:

       1
       1/1
       1/2
       2/3
       3/5
       5/8
       ...

   For the first three spiral levels the intended values are represented
   by rational pairs rather than storing a floating-point PHI constant.

7. LEARNED MODE IS NEVER CALLED EXACT
   -----------------------------------
   --shortcut-search
       no learned skipping
       exhaustive over its stated factor domain and enabled filters

   --learned-shortcut-search
       uses gap skipping
       heuristic
       may skip a valid candidate

8. FILTER AUDIT
   --------------
   v8 records exactly where candidates disappear.

9. LARGE INTEGER SAFE OUTPUT
   --------------------------
   All large integers are formatted with grouped decimal separators.

10. RESUME
    ------
    --resume-from P

    starts prime generation at the requested p.

    A recovered learner state whose last observation precedes the
    requested resume point is treated as a historical state only.
    Its old active skip interval is not blindly projected into the
    new run.

===============================================================================
"""

import sys
import time
import math
import os
import json
from typing import List, Tuple, Optional, Dict, Iterable


# =============================================================================
# CONFIGURATION
# =============================================================================

START = 5
DEFAULT_LIMIT = 999_999_999

DISPLAY_INTERVAL = 0.50
SEGMENT_SIZE = 524_288

LEARNING_LOG = "cubic_frobenius_620_learning_v8.log"
LEARNING_STATE = "cubic_frobenius_620_learning_v8.state"

CHECKPOINT_EVERY = 50_000

SHORTCUT_LOG_LINES = 10
LINEAR_LOG_LINES = 8

Q_SCREEN_PRIMES = (
    7, 11, 13, 17, 19, 23,
    29, 31, 37, 41, 43, 47
)

# Cubic Frobenius polynomial:
#
#     x^3 - x^2 - x - 1
#
CUBIC_F = [-1, -1, -1, 1]

# discriminant = -44
CUBIC_DISCRIMINANT_ABS = 44

SMALL_PRIME_POOL = (
    2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
    31, 37, 41, 43, 47, 53, 59, 61, 67,
    71, 73, 79, 83, 89, 97, 101, 103, 107,
    109, 113, 127, 131, 137, 139, 149, 151,
    157, 163, 167, 173, 179, 181, 191,
    193, 197, 199, 211, 223, 227, 229,
    233, 239, 241, 251, 257, 263, 269,
    271, 277, 281, 283, 293, 307, 311,
    313, 317, 331, 337, 347, 349, 353,
    359, 367, 373, 379, 383, 389, 397,
    401, 409, 419, 421, 431, 433, 439,
    443, 449, 457, 461, 463, 467, 479,
    487, 491, 499, 503, 509, 521, 523,
    541, 547, 557, 563, 569, 571, 577,
    587, 593, 599, 601, 607, 613, 617,
    619, 631, 641, 643, 647, 653, 659,
    661, 673, 677, 683, 691, 701, 709,
    719, 727, 733, 739, 743, 751, 757,
    761, 769, 773, 787, 797, 809, 811,
    821, 823, 827, 829, 839, 853, 857,
    859, 863, 877, 881, 883, 887, 907,
    911, 919, 929, 937, 941, 947, 953,
    967, 971, 977, 983, 991, 997
)

ANSI = sys.stdout.isatty()

if ANSI:
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    RED = "\033[31m"
    CYAN = "\033[36m"
    MAGENTA = "\033[35m"
else:
    RESET = BOLD = DIM = GREEN = YELLOW = RED = CYAN = MAGENTA = ""


# =============================================================================
# DISPLAY HELPERS
# =============================================================================

def fmt(n: int) -> str:
    return f"{n:,}"


def yesno(v: bool) -> str:
    return "YES" if v else "NO"


def pct(a: int, b: int) -> str:
    if b <= 0:
        return "0.000%"
    return f"{100.0 * a / b:.3f}%"


def safe_int(s: str) -> Optional[int]:
    try:
        return int(s.replace(",", "").replace("_", ""))
    except Exception:
        return None


# =============================================================================
# INTEGER MATH
# =============================================================================

def ext_gcd_int(a: int, b: int) -> Tuple[int, int, int]:
    old_r, r = a, b
    old_s, s = 1, 0
    old_t, t = 0, 1

    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
        old_t, t = t, old_t - q * t

    return old_r, old_s, old_t


def mod_inv_int(a: int, m: int) -> int:
    a %= m
    g, x, _ = ext_gcd_int(a, m)

    if g != 1:
        raise ValueError(
            f"non-unit inverse: gcd({a},{m})={g}"
        )

    return x % m


# =============================================================================
# POLYNOMIAL ARITHMETIC
# =============================================================================

def poly_clean(a: List[int]) -> List[int]:
    a = list(a)

    while len(a) > 1 and a[-1] == 0:
        a.pop()

    return a


def poly_add(a: List[int], b: List[int], mod: int) -> List[int]:
    n = max(len(a), len(b))
    out = [0] * n

    for i in range(n):
        av = a[i] if i < len(a) else 0
        bv = b[i] if i < len(b) else 0
        out[i] = (av + bv) % mod

    return poly_clean(out)


def poly_sub(a: List[int], b: List[int], mod: int) -> List[int]:
    n = max(len(a), len(b))
    out = [0] * n

    for i in range(n):
        av = a[i] if i < len(a) else 0
        bv = b[i] if i < len(b) else 0
        out[i] = (av - bv) % mod

    return poly_clean(out)


def poly_mul(a: List[int], b: List[int], mod: int) -> List[int]:
    if a == [0] or b == [0]:
        return [0]

    out = [0] * (len(a) + len(b) - 1)

    for i, av in enumerate(a):
        if av == 0:
            continue

        for j, bv in enumerate(b):
            if bv:
                out[i + j] = (
                    out[i + j] + av * bv
                ) % mod

    return poly_clean(out)


def poly_make_monic(a: List[int], mod: int) -> List[int]:
    a = poly_clean([x % mod for x in a])

    if a == [0]:
        return [0]

    inv = mod_inv_int(a[-1], mod)

    return poly_clean([
        (x * inv) % mod
        for x in a
    ])


def poly_divmod(
    a: List[int],
    b: List[int],
    mod: int
) -> Tuple[List[int], List[int]]:

    a = poly_clean([x % mod for x in a])
    b = poly_clean([x % mod for x in b])

    if b == [0]:
        raise ZeroDivisionError("polynomial division by zero")

    if len(a) < len(b):
        return [0], a

    q = [0] * (len(a) - len(b) + 1)
    r = a[:]

    inv_lead = mod_inv_int(b[-1], mod)

    while r != [0] and len(r) >= len(b):
        d = len(r) - len(b)

        coeff = (r[-1] * inv_lead) % mod
        q[d] = coeff

        for i in range(len(b)):
            r[i + d] = (
                r[i + d] - coeff * b[i]
            ) % mod

        r = poly_clean(r)

    return poly_clean(q), poly_clean(r)


def poly_mod(a: List[int], mod_poly: List[int], mod: int) -> List[int]:
    return poly_divmod(a, mod_poly, mod)[1]


def poly_gcd(
    a: List[int],
    b: List[int],
    mod: int
) -> List[int]:

    a = poly_clean(a)
    b = poly_clean(b)

    while b != [0]:
        _, r = poly_divmod(a, b, mod)
        a, b = b, r

    return poly_make_monic(a, mod)


def poly_powmod(
    base: List[int],
    exponent: int,
    modulus_poly: List[int],
    mod: int
) -> List[int]:

    result = [1]
    base = poly_mod(base, modulus_poly, mod)

    while exponent:
        if exponent & 1:
            result = poly_mod(
                poly_mul(result, base, mod),
                modulus_poly,
                mod
            )

        exponent >>= 1

        if exponent:
            base = poly_mod(
                poly_mul(base, base, mod),
                modulus_poly,
                mod
            )

    return poly_clean(result)


# =============================================================================
# CUBIC FROBENIUS
# =============================================================================

class CubicFrobeniusResult:
    __slots__ = (
        "passed",
        "gcd_discriminant",
        "x_n",
        "reason",
        "factor_degrees"
    )

    def __init__(
        self,
        passed: bool,
        gcd_discriminant: int,
        x_n: List[int],
        reason: str,
        factor_degrees: Tuple[int, ...] = ()
    ):
        self.passed = passed
        self.gcd_discriminant = gcd_discriminant
        self.x_n = x_n
        self.reason = reason
        self.factor_degrees = factor_degrees


class FrobeniusAuditor:
    """
    Independent cubic Frobenius test.

    Polynomial:

        f(x) = x^3 - x^2 - x - 1

    Prime Frobenius congruence:

        x^n == x mod (n, f)

    Therefore the direct test is:

        x^n mod f == x

    The polynomial discriminant is -44.

    gcd(n,44) is reported but does not automatically reject a candidate.
    """

    def __init__(self, n: int, polynomial: List[int] = CUBIC_F):
        self.n = n
        self.f = polynomial

    def execute_audit(self) -> CubicFrobeniusResult:
        n = self.n

        if n <= 1:
            return CubicFrobeniusResult(
                False, 1, [0], "INVALID_N"
            )

        g44 = math.gcd(n, CUBIC_DISCRIMINANT_ABS)

        x = [0, 1]

        try:
            x_n = poly_powmod(
                x,
                n,
                self.f,
                n
            )
        except ValueError as exc:
            return CubicFrobeniusResult(
                False,
                g44,
                [0],
                f"MODULAR_COLLAPSE:{exc}"
            )

        x_n = poly_clean([
            z % n
            for z in x_n
        ])

        passed = x_n == [0, 1]

        reason = (
            "FROBENIUS_PASS"
            if passed
            else "FROBENIUS_FAIL"
        )

        return CubicFrobeniusResult(
            passed,
            g44,
            x_n,
            reason
        )


# =============================================================================
# DETERMINISTIC / PROBABLE PRIME
# =============================================================================

def is_prime_fast(n: int) -> bool:
    """
    Deterministic Miller-Rabin for n < 2^64.

    For n >= 2^64, the supplied base set is treated as probable-prime
    evidence rather than a mathematical certificate.
    """

    if n < 2:
        return False

    small = (
        2, 3, 5, 7, 11, 13, 17, 19,
        23, 29, 31, 37
    )

    for p in small:
        if n == p:
            return True
        if n % p == 0:
            return False

    d = n - 1
    s = 0

    while not d & 1:
        s += 1
        d >>= 1

    if n < (1 << 64):
        bases = (
            2, 325, 9375, 28178,
            450775, 9780504, 1795265022
        )
    else:
        bases = (
            2, 3, 5, 7, 11, 13,
            17, 19, 23, 29, 31, 37
        )

    for a in bases:
        a %= n

        if a in (0, 1):
            continue

        x = pow(a, d, n)

        if x in (1, n - 1):
            continue

        witness = True

        for _ in range(s - 1):
            x = (x * x) % n

            if x == n - 1:
                witness = False
                break

        if witness:
            return False

    return True


# =============================================================================
# SEGMENTED PRIME GENERATOR
# =============================================================================

def base_primes_upto(limit: int) -> List[int]:
    if limit < 2:
        return []

    sieve = bytearray(b"\x01") * (limit + 1)
    sieve[0:2] = b"\x00\x00"

    root = math.isqrt(limit)

    for p in range(2, root + 1):
        if sieve[p]:
            start = p * p
            sieve[start:limit + 1:p] = (
                b"\x00"
                * (((limit - start) // p) + 1)
            )

    return [
        i for i, v in enumerate(sieve)
        if v
    ]


def segmented_prime_stream(
    start: int,
    end: int,
    segment_size: int = SEGMENT_SIZE
) -> Iterable[int]:

    if end <= 2 or start >= end:
        return

    start = max(start, 2)

    base = base_primes_upto(
        math.isqrt(end - 1)
    )

    low = start

    while low < end:
        high = min(
            low + segment_size,
            end
        )

        block = bytearray(
            b"\x01"
            * (high - low)
        )

        for p in base:
            pp = p * p

            if pp >= high:
                break

            first = max(
                pp,
                ((low + p - 1) // p) * p
            )

            for x in range(
                first,
                high,
                p
            ):
                block[x - low] = 0

        for i, ok in enumerate(block):
            if ok:
                yield low + i

        low = high


# =============================================================================
# FIBONACCI
# =============================================================================

def fibonacci_pair_mod(
    n: int,
    mod: int
) -> Tuple[int, int]:

    if n == 0:
        return 0, 1

    a, b = fibonacci_pair_mod(
        n >> 1,
        mod
    )

    c = (
        a * ((2 * b - a) % mod)
    ) % mod

    d = (
        a * a + b * b
    ) % mod

    if n & 1:
        return d, (c + d) % mod

    return c, d


def fibonacci_and_lucas_mod(
    n: int,
    mod: int
) -> Tuple[int, int]:

    fn, fn1 = fibonacci_pair_mod(
        n,
        mod
    )

    # L_n = F_{n-1} + F_{n+1}
    #
    # F_{n-1} = F_{n+1} - F_n
    #
    # therefore:
    #
    # L_n = 2F_{n+1} - F_n

    ln = (
        2 * fn1 - fn
    ) % mod

    return fn, ln


# =============================================================================
# JACOBI
# =============================================================================

def jacobi_symbol(a: int, n: int) -> int:
    if n <= 0 or n % 2 == 0:
        raise ValueError("Jacobi modulus must be positive odd")

    a %= n
    result = 1

    while a:
        while a % 2 == 0:
            a //= 2
            r = n % 8

            if r in (3, 5):
                result = -result

        a, n = n, a

        if a % 4 == 3 and n % 4 == 3:
            result = -result

        a %= n

    return result if n == 1 else 0


# =============================================================================
# GENERIC LUCAS / COMPANION MATRIX
# =============================================================================

def mat_mul_2x2_mod(
    A: Tuple[Tuple[int, int], Tuple[int, int]],
    B: Tuple[Tuple[int, int], Tuple[int, int]],
    mod: int
):
    return (
        (
            (A[0][0] * B[0][0] +
             A[0][1] * B[1][0]) % mod,

            (A[0][0] * B[0][1] +
             A[0][1] * B[1][1]) % mod
        ),
        (
            (A[1][0] * B[0][0] +
             A[1][1] * B[1][0]) % mod,

            (A[1][0] * B[0][1] +
             A[1][1] * B[1][1]) % mod
        )
    )


def mat_pow_2x2_mod(
    A: Tuple[Tuple[int, int], Tuple[int, int]],
    e: int,
    mod: int
):
    R = (
        (1, 0),
        (0, 1)
    )

    while e:
        if e & 1:
            R = mat_mul_2x2_mod(
                R, A, mod
            )

        e >>= 1

        if e:
            A = mat_mul_2x2_mod(
                A, A, mod
            )

    return R


def lucas_uv_mod(
    P: int,
    Q: int,
    n: int,
    k: int
) -> Tuple[int, int]:

    if k == 0:
        return 0, 2 % n

    # Companion matrix:
    #
    # [ P  -Q ]
    # [ 1   0 ]
    #
    # M^k =
    #
    # [ U_{k+1}  -Q U_k ]
    # [ U_k       -Q U_{k-1} ]

    M = (
        (P % n, (-Q) % n),
        (1, 0)
    )

    R = mat_pow_2x2_mod(
        M,
        k,
        n
    )

    Uk = R[1][0]

    if Q % n == 0:
        Uk1 = R[0][0]
    else:
        # P*U_k - Q*U_{k-1} = U_{k+1}
        #
        # avoid division by Q
        #
        # Direct recurrence extraction from matrix:
        Uk1 = R[0][0]

    Vk = (
        2 * Uk1 - P * Uk
    ) % n

    return Uk, Vk


# =============================================================================
# QUADRATIC VANTAGE D=5
# =============================================================================

def quadratic_closure_audit(
    n: int
) -> Tuple[bool, Dict[str, object]]:

    fn, fn1 = fibonacci_pair_mod(
        n,
        n
    )

    fnm1 = (fn1 - fn) % n

    # Lucas:
    ln = (2 * fn1 - fn) % n

    axis_a = (
        ln == 1 % n
    )

    r5 = n % 5

    if r5 == 0:
        expected_fn = 0
    elif r5 in (1, 4):
        expected_fn = 1
    else:
        expected_fn = n - 1

    axis_b = (
        fn == expected_fn
    )

    if r5 == 0:
        expected_matrix = (
            (3 % n, 0),
            (0, 3 % n)
        )
    elif r5 in (1, 4):
        expected_matrix = (
            (1 % n, 1 % n),
            (1 % n, 0)
        )
    else:
        expected_matrix = (
            (0, n - 1),
            (n - 1, 1 % n)
        )

    actual_matrix = (
        (fn1, fn),
        (fn, fnm1)
    )

    axis_c = (
        actual_matrix == expected_matrix
    )

    passed = (
        axis_a and
        axis_b and
        axis_c
    )

    return passed, {
        "F_n": fn,
        "F_n1": fn1,
        "F_nm1": fnm1,
        "L_n": ln,
        "axis_A": axis_a,
        "axis_B": axis_b,
        "axis_C": axis_c,
        "r5": r5
    }


# =============================================================================
# BINARY VANTAGE D=8
# =============================================================================

def binary_vantage(n: int) -> Tuple[bool, Dict[str, object]]:
    if n % 2 == 0:
        return False, {
            "reason": "EVEN"
        }

    if math.gcd(n, 8) != 1:
        return False, {
            "reason": "GCD_8"
        }

    chi = jacobi_symbol(8, n)

    if chi == 0:
        return False, {
            "reason": "JACOBI_ZERO"
        }

    k = n - chi

    U, V = lucas_uv_mod(
        -2,
        -1,
        n,
        k
    )

    expected_v = (
        2
        if chi == 1
        else -2
    ) % n

    passed = (
        U == 0 and
        V == expected_v
    )

    return passed, {
        "chi": chi,
        "k": k,
        "U": U,
        "V": V,
        "expected_V": expected_v
    }


# =============================================================================
# TRINARY VANTAGE D=13
# =============================================================================

def trinary_vantage(n: int) -> Tuple[bool, Dict[str, object]]:
    if n % 2 == 0:
        return False, {
            "reason": "EVEN"
        }

    if n == 13:
        return True, {
            "reason": "DISCRIMINANT_BOUNDARY"
        }

    if math.gcd(n, 13) != 1:
        return False, {
            "reason": "GCD_13"
        }

    chi = jacobi_symbol(13, n)

    if chi == 0:
        return False, {
            "reason": "JACOBI_ZERO"
        }

    k = n - chi

    U, V = lucas_uv_mod(
        -3,
        -1,
        n,
        k
    )

    expected_v = (
        2
        if chi == 1
        else -2
    ) % n

    passed = (
        U == 0 and
        V == expected_v
    )

    return passed, {
        "chi": chi,
        "k": k,
        "U": U,
        "V": V,
        "expected_V": expected_v
    }


# =============================================================================
# $620 VERIFIER
# =============================================================================

def verify_620(n: int) -> Dict[str, object]:

    prime = is_prime_fast(n)

    residue_ok = (
        n % 5 in (2, 3)
    )

    base2_ok = (
        pow(2, n - 1, n) == 1
    )

    fn, _ = fibonacci_pair_mod(
        n,
        n
    )

    fib_ok = (
        fn == 0
    )

    candidate = (
        (not prime) and
        residue_ok and
        base2_ok and
        fib_ok
    )

    return {
        "prime": prime,
        "residue_ok": residue_ok,
        "base2_ok": base2_ok,
        "fib_ok": fib_ok,
        "620_candidate": candidate
    }


# =============================================================================
# SMALL FACTORIZATION
# =============================================================================

def factor_small(
    n: int,
    limit: int = 1_000_000
) -> List[int]:

    factors = []
    x = n

    for p in SMALL_PRIME_POOL:
        if p > limit:
            break

        if p * p > x:
            break

        while x % p == 0:
            factors.append(p)
            x //= p

        if x == 1:
            break

    if x > 1:
        factors.append(x)

    return factors


# =============================================================================
# Q SCREEN
# =============================================================================

def q_small_prime_screen(q: int) -> bool:
    for r in Q_SCREEN_PRIMES:
        if q != r and q % r == 0:
            return False

    return True


# =============================================================================
# FACTOR STRATEGIES
# =============================================================================

STRATEGIES = {
    "3p-2": (3, -2),
    "2p+1": (2, 1),
    "7p-6": (7, -6),
}


def strategy_values(name: str) -> Tuple[int, int]:
    if name not in STRATEGIES:
        raise ValueError(
            f"unknown strategy: {name}"
        )

    return STRATEGIES[name]


def strategy_mod5_classes(
    a: int,
    b: int
) -> Tuple[int, ...]:

    out = []

    for r in range(5):
        if r == 0:
            continue

        q = (a * r + b) % 5
        nmod = (r * q) % 5

        if nmod in (2, 3):
            out.append(r)

    return tuple(out)


def max_p_for_strategy(
    limit: int,
    a: int,
    b: int
) -> int:

    # Find largest p with:
    #
    #     p(ap+b) < limit

    if a <= 0:
        raise ValueError(
            "strategy coefficient a must be positive"
        )

    lo = 0
    hi = max(
        1,
        math.isqrt(
            max(1, limit // a)
        ) + 2
    )

    while hi * (a * hi + b) < limit:
        hi *= 2

    while lo + 1 < hi:
        mid = (lo + hi) // 2

        if mid * (a * mid + b) < limit:
            lo = mid
        else:
            hi = mid

    return lo


# =============================================================================
# EXACT FACTORIZED FERMAT
# =============================================================================

def factor_reduced_fermat(
    n: int,
    p: int,
    q: int
) -> bool:

    # p and q are already established as prime.
    #
    # Therefore exponents can be reduced modulo p-1/q-1.

    ep = (n - 1) % (p - 1)
    eq = (n - 1) % (q - 1)

    return (
        pow(2, ep, p) == 1 and
        pow(2, eq, q) == 1
    )


# =============================================================================
# LEARNED GAP SPIRAL
# =============================================================================

class GapSpiralController:
    """
    HEURISTIC accelerator.

    Exact rational spiral fractions:

        1
        5/8
        3/8

    are used instead of storing a floating-point PHI constant.

    The decimal value of 5/8 is not used to define the state.

    v8 persists complete state.
    """

    SPIRAL_FRACTIONS = (
        (1, 1),
        (5, 8),
        (3, 8),
    )

    COOLDOWN_AFTER_TIER_HIT = 5

    def __init__(
        self,
        log_path: str = LEARNING_LOG,
        state_path: str = LEARNING_STATE
    ):
        self.log_path = log_path
        self.state_path = state_path

        self.last_hit_p: Optional[int] = None
        self.cycle_index = 0

        self.active_skip_start: Optional[int] = None
        self.active_skip_end: Optional[int] = None

        self.cooldown_remaining = 0

        self.observations = 0
        self.useful_observations = 0
        self.total_skipped = 0
        self.arm_events = 0

        self.last_gap = 0
        self.last_fraction_num = 1
        self.last_fraction_den = 1
        self.last_skip_size = 0

        self.tier_hits = 0

        self.recovered = False

    # -------------------------------------------------------------------------
    # FRACTION
    # -------------------------------------------------------------------------

    def fraction_pair(self) -> Tuple[int, int]:
        return self.SPIRAL_FRACTIONS[
            min(
                self.cycle_index,
                len(self.SPIRAL_FRACTIONS) - 1
            )
        ]

    def fraction_text(self) -> str:
        a, b = self.fraction_pair()
        return f"{a}/{b}"

    # -------------------------------------------------------------------------
    # LOAD / SAVE
    # -------------------------------------------------------------------------

    def load(self) -> bool:
        if not os.path.exists(self.state_path):
            return False

        try:
            with open(
                self.state_path,
                "r",
                encoding="utf-8"
            ) as f:
                state = json.load(f)

            self.last_hit_p = state.get(
                "last_hit_p"
            )

            self.cycle_index = int(
                state.get("cycle_index", 0)
            )

            self.active_skip_start = state.get(
                "active_skip_start"
            )

            self.active_skip_end = state.get(
                "active_skip_end"
            )

            self.cooldown_remaining = int(
                state.get(
                    "cooldown_remaining",
                    0
                )
            )

            self.observations = int(
                state.get(
                    "observations",
                    0
                )
            )

            self.useful_observations = int(
                state.get(
                    "useful_observations",
                    0
                )
            )

            self.total_skipped = int(
                state.get(
                    "total_skipped",
                    0
                )
            )

            self.arm_events = int(
                state.get(
                    "arm_events",
                    0
                )
            )

            self.last_gap = int(
                state.get(
                    "last_gap",
                    0
                )
            )

            self.last_fraction_num = int(
                state.get(
                    "last_fraction_num",
                    1
                )
            )

            self.last_fraction_den = int(
                state.get(
                    "last_fraction_den",
                    1
                )
            )

            self.last_skip_size = int(
                state.get(
                    "last_skip_size",
                    0
                )
            )

            self.tier_hits = int(
                state.get(
                    "tier_hits",
                    0
                )
            )

            self.recovered = True
            return True

        except Exception:
            return False

    def save(self) -> None:
        state = {
            "version": 8,
            "last_hit_p": self.last_hit_p,
            "cycle_index": self.cycle_index,
            "active_skip_start": self.active_skip_start,
            "active_skip_end": self.active_skip_end,
            "cooldown_remaining": self.cooldown_remaining,
            "observations": self.observations,
            "useful_observations": self.useful_observations,
            "total_skipped": self.total_skipped,
            "arm_events": self.arm_events,
            "last_gap": self.last_gap,
            "last_fraction_num": self.last_fraction_num,
            "last_fraction_den": self.last_fraction_den,
            "last_skip_size": self.last_skip_size,
            "tier_hits": self.tier_hits,
            "saved_at": time.time(),
        }

        tmp = self.state_path + ".tmp"

        with open(
            tmp,
            "w",
            encoding="utf-8"
        ) as f:
            json.dump(
                state,
                f,
                separators=(",", ":")
            )

        os.replace(
            tmp,
            self.state_path
        )

    def reset_active_for_resume(
        self,
        resume_from: int
    ) -> None:

        # Historical observations remain.
        #
        # But an active skip interval generated before this run must not
        # silently cross the new resume boundary.

        if (
            self.active_skip_end is not None and
            self.active_skip_end >= resume_from
        ):
            self.active_skip_start = None
            self.active_skip_end = None

        if (
            self.last_hit_p is not None and
            self.last_hit_p < resume_from
        ):
            # Keep historical statistics, but don't form a fresh gap
            # from an unrelated previous run.
            self.last_hit_p = None

    # -------------------------------------------------------------------------
    # DECISION
    # -------------------------------------------------------------------------

    def decide_skip(self, p: int) -> bool:
        if self.cooldown_remaining > 0:
            return False

        if (
            self.active_skip_start is None or
            self.active_skip_end is None
        ):
            return False

        if self.active_skip_start <= p <= self.active_skip_end:
            self.total_skipped += 1
            return True

        if p > self.active_skip_end:
            self.active_skip_start = None
            self.active_skip_end = None

        return False

    # -------------------------------------------------------------------------
    # OBSERVE
    # -------------------------------------------------------------------------

    def observe(
        self,
        p: int,
        cubic_passed: bool
    ) -> None:

        self.observations += 1

        if self.cooldown_remaining > 0:
            self.cooldown_remaining -= 1

            if self.cooldown_remaining == 0:
                self.active_skip_start = None
                self.active_skip_end = None

            if (
                self.observations %
                CHECKPOINT_EVERY
                == 0
            ):
                self.save()

            return

        if not cubic_passed:
            if (
                self.observations %
                CHECKPOINT_EVERY
                == 0
            ):
                self.save()

            return

        self.useful_observations += 1

        if self.last_hit_p is not None:
            gap = p - self.last_hit_p

            if gap > 0:
                fn, fd = self.fraction_pair()

                skip_size = (
                    gap * fn
                ) // fd

                self.last_gap = gap
                self.last_fraction_num = fn
                self.last_fraction_den = fd
                self.last_skip_size = skip_size

                if skip_size > 0:
                    self.active_skip_start = p + 1
                    self.active_skip_end = (
                        p + skip_size
                    )

                    self.arm_events += 1

                self.cycle_index = min(
                    self.cycle_index + 1,
                    len(self.SPIRAL_FRACTIONS) - 1
                )

        self.last_hit_p = p

        if (
            self.observations %
            CHECKPOINT_EVERY
            == 0
        ):
            self.save()

    # -------------------------------------------------------------------------
    # TIER HIT
    # -------------------------------------------------------------------------

    def record_tier_hit(
        self,
        p: int,
        tier: str
    ) -> None:

        self.tier_hits += 1

        self.active_skip_start = None
        self.active_skip_end = None

        self.cooldown_remaining = (
            self.COOLDOWN_AFTER_TIER_HIT
        )

        self.last_hit_p = None
        self.cycle_index = 0

        self.save()

        with open(
            self.log_path,
            "a",
            encoding="utf-8"
        ) as f:

            f.write(
                "TIER_HIT|"
                f"p={p}|"
                f"tier={tier}|"
                f"cooldown={self.cooldown_remaining}\n"
            )

    # -------------------------------------------------------------------------
    # SUMMARY
    # -------------------------------------------------------------------------

    def summary(self) -> str:
        return (
            f"obs={fmt(self.observations)} "
            f"useful={fmt(self.useful_observations)} "
            f"cycle={self.cycle_index} "
            f"fraction={self.fraction_text()} "
            f"skipped={fmt(self.total_skipped)} "
            f"cooldown={self.cooldown_remaining}"
        )


# =============================================================================
# FILTER COUNTERS
# =============================================================================

class Counters:
    def __init__(self):
        self.candidate_pairs = 0

        self.q_modular_attempts = 0
        self.q_modular_rejects = 0

        self.q_primality_tests = 0
        self.q_rejected = 0

        self.mod5_attempts = 0
        self.mod5_rejected = 0

        self.fermat_attempts = 0
        self.fermat_rejected = 0

        self.quadratic_attempts = 0
        self.quadratic_survivors = 0
        self.quadratic_rejected = 0

        self.binary_attempts = 0
        self.binary_survivors = 0
        self.binary_rejected = 0

        self.trinary_attempts = 0
        self.trinary_survivors = 0
        self.trinary_rejected = 0

        self.cubic_attempts = 0
        self.cubic_survivors = 0
        self.cubic_rejected = 0

        self.cubic_collapses = 0

        self.final_verifications = 0
        self.winners = 0

        self.learned_skips = 0

        self.primes_examined = 0
        self.primes_mod5_skipped = 0

    def as_dict(self):
        return self.__dict__.copy()


# =============================================================================
# TERMINAL DASHBOARD
# =============================================================================

class Dashboard:
    def __init__(
        self,
        limit: int,
        strategy: str,
        learned: bool
    ):
        self.limit = limit
        self.strategy = strategy
        self.learned = learned

        self.started = time.time()
        self.last_render = 0.0

    def render(
        self,
        current_p: int,
        max_p: int,
        counters: Counters,
        learner: Optional[GapSpiralController],
        phase: str = "SEARCHING"
    ) -> None:

        now = time.time()

        if (
            now - self.last_render <
            DISPLAY_INTERVAL
        ):
            return

        self.last_render = now

        elapsed = now - self.started

        progress = (
            100.0 * current_p / max_p
            if max_p > 0
            else 0.0
        )

        speed = (
            counters.candidate_pairs / elapsed
            if elapsed > 0
            else 0.0
        )

        if ANSI:
            print("\033[2J\033[H", end="")

        print()
        print(
            "╔" + "═" * 91 + "╗"
        )
        print(
            "║"
            + " GAP-SPIRAL / MULTI-VANTAGE SHORTCUT PIPELINE v8 "
            .center(91)
            + "║"
        )
        print(
            "╠" + "═" * 91 + "╣"
        )

        rows = [
            ("Elapsed Runtime", f"{elapsed:,.3f} s"),
            ("Search Phase", phase),
            ("Factor Strategy", self.strategy),
            (
                "Prime Seed",
                f"p = {fmt(current_p)}"
            ),
            (
                "Limit",
                fmt(self.limit)
            ),
            (
                "Max p",
                fmt(max_p)
            ),
            (
                "p Search Progress",
                f"{progress:.3f}% / max p = {fmt(max_p)}"
            ),
            (
                "Candidate Pairs Tested",
                fmt(counters.candidate_pairs)
            ),
            (
                "Candidate Pair Rate",
                f"{speed:,.2f}/s"
            ),
            (
                "Prime Values Examined",
                fmt(counters.primes_examined)
            ),
            (
                "Prime mod-5 Skips",
                fmt(counters.primes_mod5_skipped)
            ),
            (
                "Learned Skips",
                fmt(counters.learned_skips)
            ),
            (
                "q Modular-Sieve Attempts",
                fmt(counters.q_modular_attempts)
            ),
            (
                "q Modular-Sieve Rejects",
                fmt(counters.q_modular_rejects)
            ),
            (
                "q Primality Tests",
                fmt(counters.q_primality_tests)
            ),
            (
                "q Rejected",
                fmt(counters.q_rejected)
            ),
            (
                "Mod-5 Attempts",
                fmt(counters.mod5_attempts)
            ),
            (
                "Mod-5 Rejected",
                fmt(counters.mod5_rejected)
            ),
            (
                "Factor-Reduced Fermat Attempts",
                fmt(counters.fermat_attempts)
            ),
            (
                "Factor-Reduced Fermat Rejects",
                fmt(counters.fermat_rejected)
            ),
            (
                "Quadratic Closure Attempts",
                fmt(counters.quadratic_attempts)
            ),
            (
                "Quadratic Closure Survivors",
                fmt(counters.quadratic_survivors)
            ),
            (
                "Quadratic Closure Rejections",
                fmt(counters.quadratic_rejected)
            ),
            (
                "Binary Vantage Attempts",
                fmt(counters.binary_attempts)
            ),
            (
                "Binary Vantage Survivors",
                fmt(counters.binary_survivors)
            ),
            (
                "Binary Vantage Rejections",
                fmt(counters.binary_rejected)
            ),
            (
                "Trinary Vantage Attempts",
                fmt(counters.trinary_attempts)
            ),
            (
                "Trinary Vantage Survivors",
                fmt(counters.trinary_survivors)
            ),
            (
                "Trinary Vantage Rejections",
                fmt(counters.trinary_rejected)
            ),
            (
                "Cubic Frobenius Attempts",
                fmt(counters.cubic_attempts)
            ),
            (
                "Cubic Frobenius Survivors",
                fmt(counters.cubic_survivors)
            ),
            (
                "Cubic Frobenius Rejections",
                fmt(counters.cubic_rejected)
            ),
            (
                "Cubic Modular Collapses",
                fmt(counters.cubic_collapses)
            ),
            (
                "$620 Final Verifications",
                fmt(counters.final_verifications)
            ),
            (
                "$620 Winners",
                fmt(counters.winners)
            ),
        ]

        for label, value in rows:
            text = (
                f"║ {label:<42}"
                f"{value:>45} ║"
            )
            print(text)

        print(
            "╠" + "═" * 91 + "╣"
        )

        if learner is not None:
            print(
                "║ "
                + (
                    "LEARNED STATE: "
                    + learner.summary()
                )[:89]
                .ljust(89)
                + " ║"
            )

        print(
            "╚" + "═" * 91 + "╝"
        )


# =============================================================================
# LOGGING
# =============================================================================

class LearningLogger:
    def __init__(
        self,
        path: str = LEARNING_LOG
    ):
        self.path = path

    def write(
        self,
        event: str,
        **fields
    ) -> None:

        with open(
            self.path,
            "a",
            encoding="utf-8"
        ) as f:

            parts = [
                event
            ]

            for k, v in fields.items():
                parts.append(
                    f"{k}={v}"
                )

            f.write(
                "|".join(parts)
                + "\n"
            )


# =============================================================================
# INSPECTION
# =============================================================================

def inspect_candidate(n: int) -> None:

    print()
    print(
        f"{BOLD}HDGL / $620 CANDIDATE INSPECTION v8{RESET}"
    )
    print(
        f"n = {fmt(n)}"
    )

    print()
    print("── BASIC ─────────────────────────────────────────────")

    prime = is_prime_fast(n)

    print(
        f"prime probable/deterministic = {yesno(prime)}"
    )
    print(
        f"n mod 5                    = {n % 5}"
    )
    print(
        f"base-2 Fermat              = "
        f"{yesno(pow(2, n - 1, n) == 1)}"
    )

    fn, ln = fibonacci_and_lucas_mod(
        n,
        n
    )

    print(
        f"F_n mod n                  = {fn}"
    )
    print(
        f"L_n mod n                  = {ln}"
    )

    print()
    print("── FACTORIZATION ─────────────────────────────────────")

    factors = factor_small(n)

    print(
        "small-factor view          = "
        + " × ".join(
            fmt(x)
            for x in factors
        )
    )

    print()
    print("── VANTAGE D=5 ───────────────────────────────────────")

    q5, d5 = quadratic_closure_audit(n)

    print(
        f"pass                       = {yesno(q5)}"
    )

    for k, v in d5.items():
        print(
            f"{k:<26} = {v}"
        )

    print()
    print("── VANTAGE D=8 ───────────────────────────────────────")

    q8, d8 = binary_vantage(n)

    print(
        f"pass                       = {yesno(q8)}"
    )

    for k, v in d8.items():
        print(
            f"{k:<26} = {v}"
        )

    print()
    print("── VANTAGE D=13 ──────────────────────────────────────")

    q13, d13 = trinary_vantage(n)

    print(
        f"pass                       = {yesno(q13)}"
    )

    for k, v in d13.items():
        print(
            f"{k:<26} = {v}"
        )

    print()
    print("── CUBIC FROBENIUS ───────────────────────────────────")

    cubic = FrobeniusAuditor(n).execute_audit()

    print(
        f"f(x)                       = "
        f"x^3 - x^2 - x - 1"
    )

    print(
        f"gcd(n,44)                  = "
        f"{cubic.gcd_discriminant}"
    )

    print(
        f"x^n mod f                 = "
        f"{cubic.x_n}"
    )

    print(
        f"Frobenius pass             = "
        f"{yesno(cubic.passed)}"
    )

    print(
        f"reason                     = "
        f"{cubic.reason}"
    )

    print()
    print("── $620 ──────────────────────────────────────────────")

    six20 = verify_620(n)

    for k, v in six20.items():
        print(
            f"{k:<26} = {v}"
        )

    print()


# =============================================================================
# SEARCH
# =============================================================================

def search_shortcut_space(
    limit_upper: int,
    ratio_strat: str = "3p-2",
    learned_mode: bool = False,
    resume_from: int = START
) -> None:

    a, b = strategy_values(
        ratio_strat
    )

    max_p = max_p_for_strategy(
        limit_upper,
        a,
        b
    )

    resume_from = max(
        START,
        resume_from
    )

    if resume_from > max_p:
        print(
            f"No search domain: resume p={fmt(resume_from)} "
            f"> max p={fmt(max_p)}"
        )
        return

    allowed_p_mod5 = strategy_mod5_classes(
        a,
        b
    )

    logger = LearningLogger()

    learner = None

    if learned_mode:
        learner = GapSpiralController()

        recovered = learner.load()

        learner.reset_active_for_resume(
            resume_from
        )

        print(
            f"[Init] Learned accelerator = ENABLED (v8 gap-spiral)"
        )

        print(
            f"[Init] Recovered "
            f"{fmt(learner.observations)} observations, "
            f"cycle_index={learner.cycle_index}, "
            f"last_hit_p={learner.last_hit_p}"
        )

    print()
    print(
        f"[Init] Limit = {fmt(limit_upper)}"
    )

    print(
        f"[Init] Ratio = {ratio_strat}"
    )

    print(
        f"[Init] Max p = {fmt(max_p)}"
    )

    print(
        f"[Init] Resume from p = {fmt(resume_from)}"
    )

    print(
        f"[Init] Exact p mod-5 classes = "
        f"{allowed_p_mod5}"
    )

    if learned_mode:
        print(
            "[Init] IMPORTANT: learned mode is HEURISTIC"
        )
    else:
        print(
            "[Init] Learned accelerator = DISABLED"
        )

    counters = Counters()

    ui = Dashboard(
        limit_upper,
        ratio_strat,
        learned_mode
    )

    current_p = resume_from

    last_winner = None

    for p in segmented_prime_stream(
        resume_from,
        max_p + 1
    ):

        current_p = p

        counters.primes_examined += 1

        if p == 3:
            continue

        if (
            p % 5 not in
            allowed_p_mod5
        ):
            counters.primes_mod5_skipped += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "MOD-5 p SKIP"
            )

            continue

        # -------------------------------------------------------------
        # HEURISTIC GAP SKIP
        # -------------------------------------------------------------

        if (
            learned_mode and
            learner is not None and
            learner.decide_skip(p)
        ):
            counters.learned_skips += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "LEARNED GAP SKIP"
            )

            continue

        q = a * p + b

        if q <= p:
            continue

        n = p * q

        if n >= limit_upper:
            break

        counters.candidate_pairs += 1

        # -------------------------------------------------------------
        # Q MODULAR SCREEN
        # -------------------------------------------------------------

        counters.q_modular_attempts += 1

        if not q_small_prime_screen(q):
            counters.q_modular_rejects += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "TESTING q"
            )

            continue

        # -------------------------------------------------------------
        # Q PRIMALITY
        # -------------------------------------------------------------

        counters.q_primality_tests += 1

        if not is_prime_fast(q):
            counters.q_rejected += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "q COMPOSITE"
            )

            continue

        # -------------------------------------------------------------
        # CANDIDATE MOD 5
        # -------------------------------------------------------------

        counters.mod5_attempts += 1

        if n % 5 not in (2, 3):
            counters.mod5_rejected += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "MOD-5 REJECT"
            )

            continue

        # -------------------------------------------------------------
        # FACTOR-REDUCED FERMAT
        # -------------------------------------------------------------

        counters.fermat_attempts += 1

        if not factor_reduced_fermat(
            n,
            p,
            q
        ):
            counters.fermat_rejected += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "FERMAT REJECT"
            )

            continue

        # -------------------------------------------------------------
        # D=5 QUADRATIC CLOSURE
        # -------------------------------------------------------------

        counters.quadratic_attempts += 1

        quadratic_passed, quadratic_data = (
            quadratic_closure_audit(n)
        )

        if not quadratic_passed:
            counters.quadratic_rejected += 1

            # Learning observes the cubic stage only.
            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "QUADRATIC REJECT"
            )

            continue

        counters.quadratic_survivors += 1

        # -------------------------------------------------------------
        # D=8
        # -------------------------------------------------------------

        counters.binary_attempts += 1

        binary_passed, binary_data = (
            binary_vantage(n)
        )

        if not binary_passed:
            counters.binary_rejected += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "BINARY REJECT"
            )

            continue

        counters.binary_survivors += 1

        # -------------------------------------------------------------
        # D=13
        # -------------------------------------------------------------

        counters.trinary_attempts += 1

        trinary_passed, trinary_data = (
            trinary_vantage(n)
        )

        if not trinary_passed:
            counters.trinary_rejected += 1

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "TRINARY REJECT"
            )

            continue

        counters.trinary_survivors += 1

        # -------------------------------------------------------------
        # CUBIC FROBENIUS
        # -------------------------------------------------------------

        counters.cubic_attempts += 1

        cubic = FrobeniusAuditor(
            n
        ).execute_audit()

        cubic_passed = cubic.passed

        if cubic.reason.startswith(
            "MODULAR_COLLAPSE"
        ):
            counters.cubic_collapses += 1

        if not cubic_passed:
            counters.cubic_rejected += 1

            if learner is not None:
                learner.observe(
                    p,
                    False
                )

            ui.render(
                current_p,
                max_p,
                counters,
                learner,
                "CUBIC REJECT"
            )

            continue

        counters.cubic_survivors += 1

        if learner is not None:
            learner.observe(
                p,
                True
            )

        # -------------------------------------------------------------
        # FINAL $620 VERIFICATION
        # -------------------------------------------------------------

        counters.final_verifications += 1

        six20 = verify_620(n)

        if six20["620_candidate"]:

            counters.winners += 1
            last_winner = n

            print()
            print(
                f"{GREEN}{BOLD}"
                "╔══════════════════════════════════════════════════╗"
                f"{RESET}"
            )
            print(
                f"{GREEN}{BOLD}"
                "║              $620 CANDIDATE FOUND              ║"
                f"{RESET}"
            )
            print(
                f"{GREEN}{BOLD}"
                "╚══════════════════════════════════════════════════╝"
                f"{RESET}"
            )

            print(
                f"p = {fmt(p)}"
            )

            print(
                f"q = {fmt(q)}"
            )

            print(
                f"n = {fmt(n)}"
            )

            print(
                f"strategy = {ratio_strat}"
            )

            print(
                f"factors = {fmt(p)} × {fmt(q)}"
            )

            print(
                f"quadratic D=5 = PASS"
            )

            print(
                f"binary D=8 = PASS"
            )

            print(
                f"trinary D=13 = PASS"
            )

            print(
                f"cubic Frobenius = PASS"
            )

            print(
                f"base-2 Fermat = PASS"
            )

            print(
                f"F_n = 0 mod n = PASS"
            )

            logger.write(
                "WINNER",
                n=n,
                p=p,
                q=q,
                strategy=ratio_strat,
                quadratic=quadratic_passed,
                binary=binary_passed,
                trinary=trinary_passed,
                cubic=cubic_passed
            )

            if learner is not None:
                learner.record_tier_hit(
                    p,
                    "$620"
                )

        ui.render(
            current_p,
            max_p,
            counters,
            learner,
            "CANDIDATE COMPLETE"
        )

    if learner is not None:
        learner.save()

    print()
    print(
        f"{BOLD}SEARCH COMPLETE{RESET}"
    )

    print(
        f"Strategy              : {ratio_strat}"
    )

    print(
        f"Limit                 : {fmt(limit_upper)}"
    )

    print(
        f"Max p                 : {fmt(max_p)}"
    )

    print(
        f"Resume from           : {fmt(resume_from)}"
    )

    print(
        f"Final p               : {fmt(current_p)}"
    )

    print(
        f"Candidate pairs       : "
        f"{fmt(counters.candidate_pairs)}"
    )

    print(
        f"Quadratic survivors   : "
        f"{fmt(counters.quadratic_survivors)}"
    )

    print(
        f"Binary survivors      : "
        f"{fmt(counters.binary_survivors)}"
    )

    print(
        f"Trinary survivors     : "
        f"{fmt(counters.trinary_survivors)}"
    )

    print(
        f"Cubic survivors       : "
        f"{fmt(counters.cubic_survivors)}"
    )

    print(
        f"$620 winners          : "
        f"{fmt(counters.winners)}"
    )

    if last_winner is not None:
        print(
            f"Last winner           : "
            f"{fmt(last_winner)}"
        )

    if learner is not None:
        print(
            f"Learned state         : "
            f"{learner.summary()}"
        )

    print()


# =============================================================================
# LINEAR $620 SEARCH
# =============================================================================

def search_620(
    start: int,
    limit: int,
    cubic_only: bool = False
) -> None:

    print()
    print(
        f"{BOLD}"
        "HDGL / $620 LINEAR SEARCH v8"
        f"{RESET}"
    )

    print(
        f"start = {fmt(start)}"
    )

    print(
        f"limit = {fmt(limit)}"
    )

    print(
        f"cubic-only = {yesno(cubic_only)}"
    )

    counters = Counters()

    started = time.time()

    last_n = start

    for n in range(
        max(5, start | 1),
        limit,
        2
    ):

        last_n = n
        counters.candidate_pairs += 1

        if cubic_only:

            counters.cubic_attempts += 1

            cubic = FrobeniusAuditor(
                n
            ).execute_audit()

            if not cubic.passed:
                counters.cubic_rejected += 1
                continue

            counters.cubic_survivors += 1

            six20 = verify_620(n)

            counters.final_verifications += 1

            if six20["620_candidate"]:
                counters.winners += 1

                print()
                print(
                    f"{GREEN}{BOLD}"
                    "$620 CANDIDATE"
                    f"{RESET}"
                )

                print(
                    f"n = {fmt(n)}"
                )

        else:

            if n % 5 not in (2, 3):
                continue

            if pow(2, n - 1, n) != 1:
                continue

            fn, _ = fibonacci_pair_mod(
                n,
                n
            )

            if fn != 0:
                continue

            if is_prime_fast(n):
                continue

            counters.final_verifications += 1

            print()
            print(
                f"{GREEN}{BOLD}"
                "$620 CANDIDATE"
                f"{RESET}"
            )

            print(
                f"n = {fmt(n)}"
            )

            cubic = FrobeniusAuditor(
                n
            ).execute_audit()

            counters.cubic_attempts += 1

            if cubic.passed:
                counters.cubic_survivors += 1
                print(
                    "cubic Frobenius = PASS"
                )
            else:
                counters.cubic_rejected += 1
                print(
                    "cubic Frobenius = FAIL"
                )

        if (
            n % 100_001 == 1
        ):
            elapsed = time.time() - started

            rate = (
                counters.candidate_pairs / elapsed
                if elapsed
                else 0
            )

            print(
                f"\r"
                f"n={fmt(n)} "
                f"tested={fmt(counters.candidate_pairs)} "
                f"rate={rate:,.2f}/s "
                f"winners={counters.winners}",
                end="",
                flush=True
            )

    print()
    print()
    print(
        f"{BOLD}LINEAR SEARCH COMPLETE{RESET}"
    )

    print(
        f"last n       = {fmt(last_n)}"
    )

    print(
        f"tested       = {fmt(counters.candidate_pairs)}"
    )

    print(
        f"winners      = {fmt(counters.winners)}"
    )


# =============================================================================
# COMMAND HELP
# =============================================================================

def print_help() -> None:

    print(
        r"""
===============================================================================
HDGL / $620 CHALLENGE HUNTER v8
===============================================================================

USAGE

  python cubic_frobenius_620_hunter_v8.py

      Inspect default candidate 2487941.

  python cubic_frobenius_620_hunter_v8.py N

      Inspect integer N.

  python cubic_frobenius_620_hunter_v8.py --search START LIMIT

      Linear $620 search.

  python cubic_frobenius_620_hunter_v8.py --cubic-search START LIMIT

      Linear cubic-Frobenius search.

  python cubic_frobenius_620_hunter_v8.py \
      --shortcut-search LIMIT [STRATEGY]

      Factor-form search without learning.

      STRATEGIES:

          3p-2
          2p+1
          7p-6

      Default:

          3p-2

  python cubic_frobenius_620_hunter_v8.py \
      --shortcut-search LIMIT STRATEGY --resume-from P

      Resume exact factor-form search from P.

  python cubic_frobenius_620_hunter_v8.py \
      --learned-shortcut-search LIMIT [STRATEGY]

      Factor-form search with the heuristic gap spiral.

  python cubic_frobenius_620_hunter_v8.py \
      --learned-shortcut-search LIMIT STRATEGY --resume-from P

      Resume the learned search from P.

===============================================================================
IMPORTANT MODES
===============================================================================

--shortcut-search

    No learned skipping.

    This is the reproducible factor-domain search.

    It still applies the enabled algebraic vantages before final $620
    verification.

--learned-shortcut-search

    Enables the gap-spiral accelerator.

    THIS MODE IS HEURISTIC.

    It may skip p values.

    Therefore absence of a $620 candidate in this mode is NOT a proof
    that no candidate exists.

--search

    Linear scan using the $620 definition.

    This is the appropriate comparison baseline for determining whether
    shortcut filters are eliminating anything that the actual $620
    definition would accept.

--cubic-search

    Linear scan with the cubic Frobenius condition evaluated first.

===============================================================================
CURRENT FACTOR FAMILY
===============================================================================

3p-2:

    q = 3p - 2
    n = p(3p-2)

2p+1:

    q = 2p + 1
    n = p(2p+1)

7p-6:

    q = 7p - 6
    n = p(7p-6)

The program derives the useful p mod-5 classes instead of blindly
testing every p.

===============================================================================
VANTAGES
===============================================================================

D=5

    Fibonacci / Lucas closure.

D=8

    P=-2
    Q=-1

D=13

    P=-3
    Q=-1

CUBIC

    f(x) = x^3 - x^2 - x - 1

    x^n == x mod (n,f)

    discriminant = -44

===============================================================================
LEARNING STATE
===============================================================================

The following file is used:

    cubic_frobenius_620_learning_v8.state

The event log is:

    cubic_frobenius_620_learning_v8.log

The state file preserves:

    last_hit_p
    cycle_index
    active skip range
    cooldown
    observations
    useful observations
    skipped count
    arm events
    last gap
    last fraction
    last skip size
    tier hits

A resume boundary prevents an old active skip interval or old
last-hit gap from silently crossing into the new run.

===============================================================================
EXAMPLE: YOUR CURRENT RUN
===============================================================================

Your displayed run resumes at:

    p = 11,904,045,228,711

For the 100,000,000,000,000,000,000,000,000,000 limit:

    python cubic_frobenius_620_hunter_v8.py \
        --learned-shortcut-search \
        100000000000000000000000000000 \
        3p-2 \
        --resume-from 11904045228711

For an exact/non-learned comparison:

    python cubic_frobenius_620_hunter_v8.py \
        --shortcut-search \
        100000000000000000000000000000 \
        3p-2 \
        --resume-from 11904045228711

For the direct linear baseline over a manageable interval:

    python cubic_frobenius_620_hunter_v8.py \
        --search \
        11904045228711 \
        11905000000000

===============================================================================
INTERPRETING THE DASHBOARD
===============================================================================

Candidate Pairs Tested
    Number of actual p,q candidate pairs constructed.

q Modular-Sieve Rejects
    q eliminated before primality testing.

q Rejected
    q failed primality.

Factor-Reduced Fermat Rejects
    Candidate failed base-2 Fermat modulo p or q.

Quadratic Closure Survivors
    Candidates surviving the D=5 closure.

Binary Vantage Survivors
    Candidates surviving D=8.

Trinary Vantage Survivors
    Candidates surviving D=13.

Cubic Frobenius Survivors
    Candidates satisfying:

        x^n == x mod (n,f)

$620 Final Verifications
    Candidates that survived every preceding algebraic filter.

$620 Winners
    Candidates satisfying the actual $620 predicate.

===============================================================================
"""
    )


# =============================================================================
# CLI
# =============================================================================

def main() -> None:

    argv = sys.argv[1:]

    if not argv:
        inspect_candidate(
            2_487_941
        )
        return

    if argv[0] in (
        "--help",
        "-h",
        "help"
    ):
        print_help()
        return

    if argv[0] == "--search":

        if len(argv) < 3:
            print_help()
            return

        start = safe_int(argv[1])
        limit = safe_int(argv[2])

        if start is None or limit is None:
            raise SystemExit(
                "START and LIMIT must be integers"
            )

        search_620(
            start,
            limit,
            cubic_only=False
        )

        return

    if argv[0] == "--cubic-search":

        if len(argv) < 3:
            print_help()
            return

        start = safe_int(argv[1])
        limit = safe_int(argv[2])

        if start is None or limit is None:
            raise SystemExit(
                "START and LIMIT must be integers"
            )

        search_620(
            start,
            limit,
            cubic_only=True
        )

        return

    if argv[0] in (
        "--shortcut-search",
        "--learned-shortcut-search"
    ):

        learned = (
            argv[0] ==
            "--learned-shortcut-search"
        )

        if len(argv) < 2:
            print_help()
            return

        limit = safe_int(argv[1])

        if limit is None:
            raise SystemExit(
                "LIMIT must be an integer"
            )

        strategy = "3p-2"
        resume_from = START

        i = 2

        while i < len(argv):

            arg = argv[i]

            if arg in STRATEGIES:
                strategy = arg
                i += 1
                continue

            if arg == "--resume-from":

                if i + 1 >= len(argv):
                    raise SystemExit(
                        "--resume-from requires P"
                    )

                resume_from = safe_int(
                    argv[i + 1]
                )

                if resume_from is None:
                    raise SystemExit(
                        "resume P must be an integer"
                    )

                i += 2
                continue

            raise SystemExit(
                f"unknown argument: {arg}"
            )

        search_shortcut_space(
            limit_upper=limit,
            ratio_strat=strategy,
            learned_mode=learned,
            resume_from=resume_from
        )

        return

    # Direct integer inspection.

    if len(argv) == 1:

        n = safe_int(argv[0])

        if n is None:
            raise SystemExit(
                f"unknown command or integer: {argv[0]}"
            )

        inspect_candidate(n)
        return

    print_help()


# =============================================================================
# ENTRY POINT
# =============================================================================

if __name__ == "__main__":
    main()