-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | Data types for the policy rules engine.

The evaluation model lives in "Ecluse.Core.Rules"; this module holds only the
dependency-light types it operates on -- the closed built-in rule vocabulary config
selects from, a rule's per-version result, and the overall decision.

A 'Rule' is __evaluation-agnostic data__: it says /what/ a rule is, never /how/ it is
evaluated. How a rule decides is a separate concern that lives in "Ecluse.Core.Rules"
('Ecluse.Core.Rules.evalRule' dispatches over this data; the engine wraps it in a
'Ecluse.Core.Rules.PreparedRule' to run it).
-}
module Ecluse.Core.Rules.Types (
    -- * The built-in rule vocabulary
    Rule (..),
    DenyIfCveParams (..),
    ruleName,

    -- * Precedence
    PrecededRule (..),
    defaultPrecedence,
    defaultAllowIfOlderThanPrecedence,
    defaultAllowIfRemediatesCvePrecedence,
    defaultAllowScopePrecedence,
    defaultDenyIfCvePrecedence,
    defaultAllowByIdentityPrecedence,
    defaultDenyInstallTimeExecutionPrecedence,
    defaultDenyByIdentityPrecedence,

    -- * Evaluation
    EvalContext (..),
    mkEvalContext,
    Reason,
    RuleVerdict (..),
    RuleEvaluation (..),
    FailureAlignment (..),
    Decision (..),

    -- * Unavailability
    Transience (..),
    RetryAfter (..),
) where

import Data.Time (NominalDiffTime, UTCTime)
import Ecluse.Core.Cve (DbEtag)
import Ecluse.Core.Package (Scope)

{- | The closed, evaluation-agnostic vocabulary of __built-in__ rules an operator
selects and refines in config. Most built-in rules reason only over the
'Ecluse.Core.Package.PackageDetails' an adapter already fetched;
'AllowIfRemediatesCve' and 'DenyIfCve' additionally consult the local advisory
database through the boot-bound 'Ecluse.Core.Rules.RuleDeps'.

This is __data, not the engine's runtime representation__: a small, inspectable,
@Eq@\/@Show@ enum so config can parse, patch (override a rule's parameters), and name
each rule. "Ecluse.Core.Rules" turns it into the engine's runtime
'Ecluse.Core.Rules.PreparedRule' (binding /how/ it is evaluated) for evaluation. It
carries no allow\/deny "direction" -- whether a rule admits or blocks is simply what
its evaluation returns.

It is also the __security boundary__ on what config can express: untrusted config only
ever yields closed 'Rule' data, never arbitrary computation. A rule whose evaluation
performs IO ('AllowIfRemediatesCve', 'DenyIfCve') is a plain constructor here that
'Ecluse.Core.Rules.evalRule' dispatches on; arbitrary evaluation closures are a
code-layer capability, never reachable from config.
-}
data Rule
    = -- | Unconditionally allow every package under the given scope.
      AllowScope Scope
    | {- | Allow a version only if it was published at least this long ago.
      Guards against race-to-publish supply-chain attacks where an attacker
      publishes a malicious version and hopes it is consumed before takedown.
      -}
      AllowIfOlderThan NominalDiffTime
    | {- | Deny any package version that runs code at install time -- npm install
      scripts, a RubyGems native-extension build, a PyPI sdist build backend -- a
      common arbitrary-code-execution vector. Yields no decision otherwise.
      -}
      DenyInstallTimeExecution
    | {- | A hard deny for a specific package or package@version. Evaluated at top
      precedence (above AllowScope) as a post-mirror revocation mechanism.
      -}
      DenyByIdentity Text
    | {- | Allow a specific package or package\@version by exact identity -- the
      allow twin of 'DenyByIdentity' and the operator's explicit escape hatch, e.g.
      for a security fix published under a version string the remediation fast
      lane's exact-match probe cannot see. Top of the allow band, still under every
      deny default.
      -}
      AllowByIdentity Text
    | {- | Fast-track a version a synced advisory names as its __exact fix__, so a
      security patch is admitted immediately rather than waiting out the
      publish-age quarantine. Effectful: it consults the local advisory database
      ('Ecluse.Core.Cve.CveLookup') through the boot-bound
      'Ecluse.Core.Rules.RuleDeps', and abstains when no database is loaded, when
      the version is not an exact fixed bound, or when the version still sits
      inside another advisory's affected range.
      -}
      AllowIfRemediatesCve
    | {- | Deny a version a synced advisory records as __affected__, at or above the
      configured severity threshold -- the deny direction over the same advisory
      database as 'AllowIfRemediatesCve', with the deliberately __opposite failure
      mode__: where an unconfirmable remediation merely falls back to the
      quarantine, an unanswerable deny check refuses the version (unless the
      operator configured it fail-open; see 'DenyIfCveParams'). Ships opt-in, not
      in the default policy: enabled before the mirror is warmed, it would deny
      the historical versions an existing build already depends on.
      -}
      DenyIfCve DenyIfCveParams
    deriving stock (Rule -> Rule -> Bool
(Rule -> Rule -> Bool) -> (Rule -> Rule -> Bool) -> Eq Rule
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Rule -> Rule -> Bool
== :: Rule -> Rule -> Bool
$c/= :: Rule -> Rule -> Bool
/= :: Rule -> Rule -> Bool
Eq, Int -> Rule -> ShowS
[Rule] -> ShowS
Rule -> String
(Int -> Rule -> ShowS)
-> (Rule -> String) -> ([Rule] -> ShowS) -> Show Rule
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Rule -> ShowS
showsPrec :: Int -> Rule -> ShowS
$cshow :: Rule -> String
show :: Rule -> String
$cshowList :: [Rule] -> ShowS
showList :: [Rule] -> ShowS
Show)

{- | 'DenyIfCve''s configured behaviour -- a separate record rather than fields on
the constructor, so its selectors stay total under the sum (@-Wpartial-fields@).
-}
data DenyIfCveParams = DenyIfCveParams
    { DenyIfCveParams -> Double
dicMinSeverity :: Double
    {- ^ The CVSS base score (0 to 10) at or above which an affecting advisory
    denies; below it the advisory is noted in the audit trail but does not block.
    A qualitative label counts as its band's ceiling, and an unscored advisory
    counts as above every threshold ('Ecluse.Core.Cve.severityAtLeast'): severity
    that cannot be proven low must not slip a deny gate.
    -}
    , DenyIfCveParams -> FailureAlignment
dicOnUnavailable :: FailureAlignment
    {- ^ How the rule resolves when the advisory database cannot answer (not
    loaded, failing, or timed out): 'FailDeny' refuses the version (fail-closed,
    the shipped default), 'FailNoDecision' skips the rule (fail-open, for the
    operator whose availability outranks a blind gate; the skip is recorded in
    the decision's audit reasons).
    -}
    }
    deriving stock (DenyIfCveParams -> DenyIfCveParams -> Bool
(DenyIfCveParams -> DenyIfCveParams -> Bool)
-> (DenyIfCveParams -> DenyIfCveParams -> Bool)
-> Eq DenyIfCveParams
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: DenyIfCveParams -> DenyIfCveParams -> Bool
== :: DenyIfCveParams -> DenyIfCveParams -> Bool
$c/= :: DenyIfCveParams -> DenyIfCveParams -> Bool
/= :: DenyIfCveParams -> DenyIfCveParams -> Bool
Eq, Int -> DenyIfCveParams -> ShowS
[DenyIfCveParams] -> ShowS
DenyIfCveParams -> String
(Int -> DenyIfCveParams -> ShowS)
-> (DenyIfCveParams -> String)
-> ([DenyIfCveParams] -> ShowS)
-> Show DenyIfCveParams
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> DenyIfCveParams -> ShowS
showsPrec :: Int -> DenyIfCveParams -> ShowS
$cshow :: DenyIfCveParams -> String
show :: DenyIfCveParams -> String
$cshowList :: [DenyIfCveParams] -> ShowS
showList :: [DenyIfCveParams] -> ShowS
Show)

{- | A stable, human-facing name for a rule -- its identity, derived from the data: the
boot-order tiebreak and the credited identity in logs and denial messages.
-}
ruleName :: Rule -> Text
ruleName :: Rule -> Text
ruleName = \case
    AllowScope{} -> Text
"AllowScope"
    AllowIfOlderThan{} -> Text
"AllowIfOlderThan"
    Rule
DenyInstallTimeExecution -> Text
"DenyInstallTimeExecution"
    DenyByIdentity{} -> Text
"DenyByIdentity"
    AllowByIdentity{} -> Text
"AllowByIdentity"
    Rule
AllowIfRemediatesCve -> Text
"AllowIfRemediatesCve"
    DenyIfCve{} -> Text
"DenyIfCve"

{- | A 'Rule' paired with the integer precedence at which it competes (higher
wins). This is config's resolved-policy element; "Ecluse.Core.Rules" prepares it into
the engine's runtime rule, whose boot-time ordering ('Ecluse.Core.Rules.bootOrder')
turns precedence -- and, at equal precedence, the rule name -- into the single total
order the engine walks.

Precedence is a __field, not an @Ord@ instance__: equal precedence between two rules
is legal (it is resolved by name in the boot order), so a total derived 'Ord' would
be non-antisymmetric -- unlawful and misleading. This mirrors
'Ecluse.Core.Version.Version', whose ordering likewise goes through a function rather
than a derived instance.
-}
data PrecededRule = PrecededRule
    { PrecededRule -> Int
rulePrecedence :: Int
    -- ^ The precedence at which this rule competes; higher wins.
    , PrecededRule -> Rule
prRule :: Rule
    -- ^ The rule itself.
    }
    deriving stock (PrecededRule -> PrecededRule -> Bool
(PrecededRule -> PrecededRule -> Bool)
-> (PrecededRule -> PrecededRule -> Bool) -> Eq PrecededRule
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PrecededRule -> PrecededRule -> Bool
== :: PrecededRule -> PrecededRule -> Bool
$c/= :: PrecededRule -> PrecededRule -> Bool
/= :: PrecededRule -> PrecededRule -> Bool
Eq, Int -> PrecededRule -> ShowS
[PrecededRule] -> ShowS
PrecededRule -> String
(Int -> PrecededRule -> ShowS)
-> (PrecededRule -> String)
-> ([PrecededRule] -> ShowS)
-> Show PrecededRule
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PrecededRule -> ShowS
showsPrec :: Int -> PrecededRule -> ShowS
$cshow :: PrecededRule -> String
show :: PrecededRule -> String
$cshowList :: [PrecededRule] -> ShowS
showList :: [PrecededRule] -> ShowS
Show)

{- | The default precedence for a rule /type/ -- used when a policy omits an
explicit precedence for a rule.

The rule types climb one ladder, most-passive to most-decisive:

@AllowIfOlderThan@ (100) < @AllowIfRemediatesCve@ (150) < @AllowScope@ (200) <
@DenyIfCve@ (225) < @AllowByIdentity@ (250) < @DenyInstallTimeExecution@ (300) <
@DenyByIdentity@ (400)@

Two placements carry the design and are worth stating plainly:

* __@DenyByIdentity@ and @DenyInstallTimeExecution@ default strictly above every
  allow__, so a blanket "any deny overrides any allow" holds for them out of the
  box: revocation and the install-script deny keep the last word.
* __@DenyIfCve@ is the deliberate exception__: it sits /below/ @AllowByIdentity@ so
  an operator's exact-identity allow -- the explicit "I have decided this specific
  version must ship" escape hatch -- overrides an advisory deny (a graceful pin for
  a false positive or an accepted risk), while still sitting /above/ the passive age
  gate, the remediation lane, and a scope allow-list, so an unpinned affected version
  is denied despite them.

An operator may still raise a /specific/ allow above a /specific/ deny (or vice
versa) with an explicit precedence -- the per-type defaults set only the
out-of-the-box ordering.
-}
defaultPrecedence :: Rule -> Int
defaultPrecedence :: Rule -> Int
defaultPrecedence = \case
    AllowIfOlderThan{} -> Int
defaultAllowIfOlderThanPrecedence
    Rule
AllowIfRemediatesCve -> Int
defaultAllowIfRemediatesCvePrecedence
    AllowScope{} -> Int
defaultAllowScopePrecedence
    DenyIfCve{} -> Int
defaultDenyIfCvePrecedence
    AllowByIdentity{} -> Int
defaultAllowByIdentityPrecedence
    Rule
DenyInstallTimeExecution -> Int
defaultDenyInstallTimeExecutionPrecedence
    DenyByIdentity{} -> Int
defaultDenyByIdentityPrecedence

{- | Default precedence of 'AllowIfOlderThan': the lowest band, a passive
quarantine that yields to an explicit allow-list and to every deny.
-}
defaultAllowIfOlderThanPrecedence :: Int
defaultAllowIfOlderThanPrecedence :: Int
defaultAllowIfOlderThanPrecedence = Int
100

{- | Default precedence of 'AllowIfRemediatesCve': above the passive age
quarantine, which is the point of the fast lane -- a security fix is admitted
immediately instead of waiting out @min-age@ -- but below 'AllowScope', so a
scoped package an operator already trusts never pays the advisory probe and the
more explicit rule keeps the audit credit.
-}
defaultAllowIfRemediatesCvePrecedence :: Int
defaultAllowIfRemediatesCvePrecedence :: Int
defaultAllowIfRemediatesCvePrecedence = Int
150

{- | Default precedence of 'AllowScope': above the passive age quarantine -- an
explicit allow-list of a trusted internal scope is a stronger statement than the
time gate -- but still below every deny.
-}
defaultAllowScopePrecedence :: Int
defaultAllowScopePrecedence :: Int
defaultAllowScopePrecedence = Int
200

{- | Default precedence of 'DenyIfCve': above the passive age gate, the
remediation lane, and a scope allow-list, so an affected version is denied
despite them -- but deliberately /below/ 'AllowByIdentity', so an operator's
exact-identity allow can pin a specific version past a false-positive or
risk-accepted advisory. The one deny type that is not strictly above every allow
(see 'defaultPrecedence').
-}
defaultDenyIfCvePrecedence :: Int
defaultDenyIfCvePrecedence :: Int
defaultDenyIfCvePrecedence = Int
225

{- | Default precedence of 'AllowByIdentity': the top of the allow band -- an
exact identity is the most explicit allow an operator can state. It sits above
'DenyIfCve' (the identity pin overrides an advisory deny) but still strictly
below the 'DenyInstallTimeExecution' and 'DenyByIdentity' defaults, so the
install-script deny and revocation keep the last word.
-}
defaultAllowByIdentityPrecedence :: Int
defaultAllowByIdentityPrecedence :: Int
defaultAllowByIdentityPrecedence = Int
250

{- | Default precedence of 'DenyInstallTimeExecution': the deny band, strictly above
every allow default, so a matching deny overrides any allow out of the box.
-}
defaultDenyInstallTimeExecutionPrecedence :: Int
defaultDenyInstallTimeExecutionPrecedence :: Int
defaultDenyInstallTimeExecutionPrecedence = Int
300

{- | Default precedence of 'DenyByIdentity': the top precedence, strictly above
every other rule (including explicit allow-lists), to serve as a hard revocation.
-}
defaultDenyByIdentityPrecedence :: Int
defaultDenyByIdentityPrecedence :: Int
defaultDenyByIdentityPrecedence = Int
400

{- | Ambient information a rule may need that is not part of the package itself:
the wall-clock "now" for age calculations, and the active advisory database's
identity for a decision's audit trail.
-}
data EvalContext = EvalContext
    { EvalContext -> UTCTime
ctxNow :: UTCTime
    -- ^ The wall-clock "now" for age-based rules.
    , EvalContext -> Maybe DbEtag
ctxAdvisoryEtag :: Maybe DbEtag
    {- ^ The advisory database 'DbEtag' active when this request was admitted, or
    'Nothing' when none is loaded (or on a path that does not consult one). It is
    the artifact a denial's audit line names as active at emit; it is
    deliberately __not__ "the database this decision was evaluated against",
    since a shadow-swap may land mid-request. Resolved once per request.
    -}
    }
    deriving stock (EvalContext -> EvalContext -> Bool
(EvalContext -> EvalContext -> Bool)
-> (EvalContext -> EvalContext -> Bool) -> Eq EvalContext
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: EvalContext -> EvalContext -> Bool
== :: EvalContext -> EvalContext -> Bool
$c/= :: EvalContext -> EvalContext -> Bool
/= :: EvalContext -> EvalContext -> Bool
Eq, Int -> EvalContext -> ShowS
[EvalContext] -> ShowS
EvalContext -> String
(Int -> EvalContext -> ShowS)
-> (EvalContext -> String)
-> ([EvalContext] -> ShowS)
-> Show EvalContext
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> EvalContext -> ShowS
showsPrec :: Int -> EvalContext -> ShowS
$cshow :: EvalContext -> String
show :: EvalContext -> String
$cshowList :: [EvalContext] -> ShowS
showList :: [EvalContext] -> ShowS
Show)

{- | Assemble the ambient evaluation context -- the __one__ assembly point for every
consumer (the packument sweep, the tarball gate, and the mirror worker's ingest
re-evaluation), so what feeds a decision is defined once, not at each call site.

The contract the single point holds: 'ctxNow' must come from the injected clock the
mount's decisions share ('Ecluse.Core.Server.Context.pdNow', which the worker's
bundle reuses), never an ad-hoc 'Data.Time.getCurrentTime', so the age gate cannot
drift between contexts; 'ctxAdvisoryEtag' is __audit-only__ (it never enters a rule's
decision), so a consumer that emits no audit line passes 'Nothing' without changing
any decision.
-}
mkEvalContext :: IO UTCTime -> IO (Maybe DbEtag) -> IO EvalContext
mkEvalContext :: IO UTCTime -> IO (Maybe DbEtag) -> IO EvalContext
mkEvalContext IO UTCTime
now IO (Maybe DbEtag)
advisoryEtag = UTCTime -> Maybe DbEtag -> EvalContext
EvalContext (UTCTime -> Maybe DbEtag -> EvalContext)
-> IO UTCTime -> IO (Maybe DbEtag -> EvalContext)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UTCTime
now IO (Maybe DbEtag -> EvalContext)
-> IO (Maybe DbEtag) -> IO EvalContext
forall a b. IO (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> IO (Maybe DbEtag)
advisoryEtag

-- | A human-facing reason a rule attaches to its result, kept for the audit trail.
type Reason = Text

{- | What a single rule returns for a single package version: a __deterministic__
verdict. The rule computes its answer -- over the package, and for the effectful rules
the advisory database -- and returns one of these. A rule cannot manufacture an
'Unavailable'; that is the distinction the resilience harness turns on. A verdict is a
decided value the harness takes at face value, never a fault it retries.

A verdict is __decisive__ iff it is 'Allow', 'Deny', or @'CannotVet' 'FailDeny' _@.
'NoDecision' and @'CannotVet' 'FailNoDecision' _@ are __non-decisive__ no-ops; the
engine collects their reasons (in boot order) for the deny-by-default audit trail.
-}
data RuleVerdict
    = -- | This rule admits the package (with a human reason). Decisive.
      Allow Reason
    | -- | This rule blocks the package (with a human reason). Decisive.
      Deny Reason
    | -- | This rule has no opinion; the reason is kept for the audit trail. A no-op.
      NoDecision Reason
    | {- | The rule reached the package but cannot vet it -- a __deterministic,
      in-process absence__, not a fault (today: no advisory database is loaded). It
      carries its own __failure alignment__: a 'FailDeny' rule is decisive
      (fail-closed, → 'Undecidable'), a 'FailNoDecision' rule is a no-op (fail-open).
      It carries __no__ 'Transience' on purpose: the absence is deterministic, so no
      in-process retry can change it -- which is exactly why the harness must not
      route it through the retry\/breaker path.
      -}
      CannotVet FailureAlignment Reason
    deriving stock (RuleVerdict -> RuleVerdict -> Bool
(RuleVerdict -> RuleVerdict -> Bool)
-> (RuleVerdict -> RuleVerdict -> Bool) -> Eq RuleVerdict
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RuleVerdict -> RuleVerdict -> Bool
== :: RuleVerdict -> RuleVerdict -> Bool
$c/= :: RuleVerdict -> RuleVerdict -> Bool
/= :: RuleVerdict -> RuleVerdict -> Bool
Eq, Int -> RuleVerdict -> ShowS
[RuleVerdict] -> ShowS
RuleVerdict -> String
(Int -> RuleVerdict -> ShowS)
-> (RuleVerdict -> String)
-> ([RuleVerdict] -> ShowS)
-> Show RuleVerdict
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RuleVerdict -> ShowS
showsPrec :: Int -> RuleVerdict -> ShowS
$cshow :: RuleVerdict -> String
show :: RuleVerdict -> String
$cshowList :: [RuleVerdict] -> ShowS
showList :: [RuleVerdict] -> ShowS
Show)

{- | The outcome the resilience harness produces for one rule: either the rule
'Decided' (any 'RuleVerdict', taken at face value), or the harness could not obtain a
verdict at all and the evaluation is 'Unavailable' -- the rule's IO threw, timed out,
or its source circuit breaker was open. __Only the harness constructs 'Unavailable'__;
a rule cannot, so the retry\/breaker machinery provably reacts only to a fault the
harness itself observed, never to a verdict a rule deliberately returned.

Decisive iff it credits a 'Decision': a decisive 'RuleVerdict', or an
@'Unavailable' _ 'FailDeny' _@. A non-decisive verdict, or an @'Unavailable' _
'FailNoDecision' _@, is a no-op whose reason is gathered for the audit trail.
-}
data RuleEvaluation
    = -- | The rule returned a verdict; the harness takes it at face value.
      Decided RuleVerdict
    | {- | The harness could not obtain a verdict: the rule's IO failed, timed out, or
      its source circuit breaker is open. It carries the rule's __failure alignment__
      (a 'FailDeny' evaluation is decisive → 'Undecidable', a 'FailNoDecision' one is a
      no-op) and a 'Transience' recording whether a retry can help. Only the harness
      builds this.
      -}
      Unavailable Transience FailureAlignment Reason
    deriving stock (RuleEvaluation -> RuleEvaluation -> Bool
(RuleEvaluation -> RuleEvaluation -> Bool)
-> (RuleEvaluation -> RuleEvaluation -> Bool) -> Eq RuleEvaluation
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RuleEvaluation -> RuleEvaluation -> Bool
== :: RuleEvaluation -> RuleEvaluation -> Bool
$c/= :: RuleEvaluation -> RuleEvaluation -> Bool
/= :: RuleEvaluation -> RuleEvaluation -> Bool
Eq, Int -> RuleEvaluation -> ShowS
[RuleEvaluation] -> ShowS
RuleEvaluation -> String
(Int -> RuleEvaluation -> ShowS)
-> (RuleEvaluation -> String)
-> ([RuleEvaluation] -> ShowS)
-> Show RuleEvaluation
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RuleEvaluation -> ShowS
showsPrec :: Int -> RuleEvaluation -> ShowS
$cshow :: RuleEvaluation -> String
show :: RuleEvaluation -> String
$cshowList :: [RuleEvaluation] -> ShowS
showList :: [RuleEvaluation] -> ShowS
Show)

{- | How a rule aligns when it cannot vet a version, or its evaluation faults.

There is deliberately __no @FailAllow@__: a failed or uncomputable check must never
/admit/ unvetted bytes. A rule whose verdict is load-bearing for safety fails
__closed__ ('FailDeny'); a remediation\/allow-direction rule whose missing signal
should not block availability fails __open__ ('FailNoDecision').
-}
data FailureAlignment
    = -- | __Fail closed.__ An uncomputable result is decisive: the version is not admitted.
      FailDeny
    | -- | __Fail open.__ An uncomputable result is a no-op: the rule simply does not fire.
      FailNoDecision
    deriving stock (FailureAlignment -> FailureAlignment -> Bool
(FailureAlignment -> FailureAlignment -> Bool)
-> (FailureAlignment -> FailureAlignment -> Bool)
-> Eq FailureAlignment
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FailureAlignment -> FailureAlignment -> Bool
== :: FailureAlignment -> FailureAlignment -> Bool
$c/= :: FailureAlignment -> FailureAlignment -> Bool
/= :: FailureAlignment -> FailureAlignment -> Bool
Eq, Int -> FailureAlignment -> ShowS
[FailureAlignment] -> ShowS
FailureAlignment -> String
(Int -> FailureAlignment -> ShowS)
-> (FailureAlignment -> String)
-> ([FailureAlignment] -> ShowS)
-> Show FailureAlignment
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FailureAlignment -> ShowS
showsPrec :: Int -> FailureAlignment -> ShowS
$cshow :: FailureAlignment -> String
show :: FailureAlignment -> String
$cshowList :: [FailureAlignment] -> ShowS
showList :: [FailureAlignment] -> ShowS
Show)

{- | The overall decision for a package version against a whole rule set.

The deciding rule is credited by __name__ ('Text'): a rule's stable identity is its
name (see 'ruleName'), independent of how it is evaluated.
-}
data Decision
    = -- | Admitted by the named rule, with its reason (was @Approved@\/@ApprovedEffectful@).
      Admitted Text Reason
    | -- | Blocked by the named rule, with its reason (was @Denied@\/@DeniedEffectful@).
      Blocked Text Reason
    | {- | No rule was decisive. Deny-by-default; carries every non-decisive reason,
      in boot order, so the denial response can explain what was considered.
      -}
      BlockedByDefault [Reason]
    | {- | Undecidable: a 'FailDeny' rule that could not be computed __won__, so the
      version could not be vetted. Fail-closed -- it is not admitted (a packument
      filters it out like a denial; a concrete artifact surfaces a @503@\/@500@ by the
      serve error model). The 'Transience' carries whether a retry can help; the
      'Reason' is the audit reason.
      -}
      Undecidable Transience Reason
    deriving stock (Decision -> Decision -> Bool
(Decision -> Decision -> Bool)
-> (Decision -> Decision -> Bool) -> Eq Decision
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Decision -> Decision -> Bool
== :: Decision -> Decision -> Bool
$c/= :: Decision -> Decision -> Bool
/= :: Decision -> Decision -> Bool
Eq, Int -> Decision -> ShowS
[Decision] -> ShowS
Decision -> String
(Int -> Decision -> ShowS)
-> (Decision -> String) -> ([Decision] -> ShowS) -> Show Decision
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Decision -> ShowS
showsPrec :: Int -> Decision -> ShowS
$cshow :: Decision -> String
show :: Decision -> String
$cshowList :: [Decision] -> ShowS
showList :: [Decision] -> ShowS
Show)

{- | Whether an unavailability is expected to resolve on its own.

This is the single distinction the serve status mapping turns on: a transient cause
('WillResolve') is worth retrying (a @503@); a permanent or internal one
('WontResolve') is not, so it must not be dressed up as a retryable @503@ (it is a
@500@). The resilience harness sets it from the nature of the failure: an upstream
outage, rate limit, timeout, or open breaker is transient; an internal or parse
fault is not.
-}
data Transience
    = {- | Transient -- a retry may succeed (an advisory source briefly down, a
      timeout, an open circuit breaker). The optional 'RetryAfter' is the delay to
      suggest to the client.
      -}
      WillResolve (Maybe RetryAfter)
    | {- | Not expected to self-heal (an internal or parse error). Retrying cannot
      help, so the request is a @500@, never a @503@.
      -}
      WontResolve
    deriving stock (Transience -> Transience -> Bool
(Transience -> Transience -> Bool)
-> (Transience -> Transience -> Bool) -> Eq Transience
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Transience -> Transience -> Bool
== :: Transience -> Transience -> Bool
$c/= :: Transience -> Transience -> Bool
/= :: Transience -> Transience -> Bool
Eq, Int -> Transience -> ShowS
[Transience] -> ShowS
Transience -> String
(Int -> Transience -> ShowS)
-> (Transience -> String)
-> ([Transience] -> ShowS)
-> Show Transience
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Transience -> ShowS
showsPrec :: Int -> Transience -> ShowS
$cshow :: Transience -> String
show :: Transience -> String
$cshowList :: [Transience] -> ShowS
showList :: [Transience] -> ShowS
Show)

{- | A @Retry-After@ delay, in whole seconds. A 'newtype' so a raw count of seconds
is never confused with some other integer when it reaches the response header.
-}
newtype RetryAfter = RetryAfter Int
    deriving stock (RetryAfter -> RetryAfter -> Bool
(RetryAfter -> RetryAfter -> Bool)
-> (RetryAfter -> RetryAfter -> Bool) -> Eq RetryAfter
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RetryAfter -> RetryAfter -> Bool
== :: RetryAfter -> RetryAfter -> Bool
$c/= :: RetryAfter -> RetryAfter -> Bool
/= :: RetryAfter -> RetryAfter -> Bool
Eq, Eq RetryAfter
Eq RetryAfter =>
(RetryAfter -> RetryAfter -> Ordering)
-> (RetryAfter -> RetryAfter -> Bool)
-> (RetryAfter -> RetryAfter -> Bool)
-> (RetryAfter -> RetryAfter -> Bool)
-> (RetryAfter -> RetryAfter -> Bool)
-> (RetryAfter -> RetryAfter -> RetryAfter)
-> (RetryAfter -> RetryAfter -> RetryAfter)
-> Ord RetryAfter
RetryAfter -> RetryAfter -> Bool
RetryAfter -> RetryAfter -> Ordering
RetryAfter -> RetryAfter -> RetryAfter
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: RetryAfter -> RetryAfter -> Ordering
compare :: RetryAfter -> RetryAfter -> Ordering
$c< :: RetryAfter -> RetryAfter -> Bool
< :: RetryAfter -> RetryAfter -> Bool
$c<= :: RetryAfter -> RetryAfter -> Bool
<= :: RetryAfter -> RetryAfter -> Bool
$c> :: RetryAfter -> RetryAfter -> Bool
> :: RetryAfter -> RetryAfter -> Bool
$c>= :: RetryAfter -> RetryAfter -> Bool
>= :: RetryAfter -> RetryAfter -> Bool
$cmax :: RetryAfter -> RetryAfter -> RetryAfter
max :: RetryAfter -> RetryAfter -> RetryAfter
$cmin :: RetryAfter -> RetryAfter -> RetryAfter
min :: RetryAfter -> RetryAfter -> RetryAfter
Ord, Int -> RetryAfter -> ShowS
[RetryAfter] -> ShowS
RetryAfter -> String
(Int -> RetryAfter -> ShowS)
-> (RetryAfter -> String)
-> ([RetryAfter] -> ShowS)
-> Show RetryAfter
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RetryAfter -> ShowS
showsPrec :: Int -> RetryAfter -> ShowS
$cshow :: RetryAfter -> String
show :: RetryAfter -> String
$cshowList :: [RetryAfter] -> ShowS
showList :: [RetryAfter] -> ShowS
Show)