requisite carries provenance and workflow state in types:
- external input becomes
Trustedonly through an explicit application policy; - finite probabilities pass through validated thresholds and a mandatory three-way
fold; - high confidence yields value-bound
Certain[A]evidence; - each
Fresh[A]owns a monotonic fetch reading and TTL, with typed stale and clock failures.
The runtime has no third-party dependencies beyond the Scala standard library.
requisite is orthogonal to Iron and
refined. Those libraries prove predicates such as non-empty,
bounded, or matching a format. requisite records where a value came from and which workflow check
it passed. A refined domain type can be carried inside Tainted, Confident, or Fresh.
Trusted means an application-supplied transition ran. It does not certify the policy's quality.
libraryDependencies += "io.github.slepp" %% "requisite" % "0.1.0"For local development, run sbt publishLocal.
import io.github.slepp.requisite.trust.*
def lookup(id: Tainted[Long, Trusted]): String =
s"customer-${id.unwrap}"
val raw = Tainted.fromInput(" 42 ")
val customer =
trySanitize(raw)(_.trim.toLongOption.toRight("invalid id"))
.map(lookup)inspect reads either state without changing it. widen deliberately lowers Trusted to
Untrusted; there is no type-safe inverse conversion.
Tainted is an opaque, allocation-free wrapper. Its runtime toString and string interpolation are
the wrapped value's representation and are not redacted. Do not interpolate sensitive tainted
values into logs.
import io.github.slepp.requisite.confidence.*
final case class Approval(customer: String, approved: Boolean)
def execute(evidence: Certain[Approval]): Unit =
val decision = evidence.value
if decision.approved then charge(decision.customer)
val decision =
Confident.from(Approval("customer-42", approved = true), 0.98).toOption.get
decision.gate.fold(
execute,
_ => requestReview(),
_ => recordOnly()
)Certain[A] contains the exact value selected by the high gate. Its implementation and
constructors are closed to ordinary Scala callers. Sensitive APIs should accept Certain[A]
alone—not evidence plus a separate value—so evidence cannot be rebound at the call boundary.
Confidence values must be finite and in 0.0..=1.0; -0.0 is canonicalized to 0.0. Custom
thresholds require likely < certain, and certain cannot be below 0.95.
Validation does not certify probability provenance, calibration, or model quality. The caller remains responsible for supplying a probability appropriate to the workflow.
fold requires all three handlers. gate.observation provides a sealed enum for pattern matching,
but incomplete-match rejection is only as strong as the downstream project's warning policy.
Constructing an observation does not construct a gate or issue evidence.
Confident.toString omits the wrapped value. A gate deliberately exposes likely and unsure values,
or the value-bound evidence for a high result.
import io.github.slepp.requisite.freshness.*
import scala.concurrent.duration.*
val quote =
Fresh.fetch(499L, 30.seconds)
.fold(error => throw new IllegalArgumentException(error.message), identity)
quote.read.fold(
failure =>
failure.fold(
expired =>
audit(expired.value)
scheduleRefresh(expired.stale),
regressed => disableCache(regressed)
),
cents => charge(cents)
)The default clock uses System.nanoTime, not wall-clock time. A custom MonotonicClock must be
thread-safe and non-decreasing across threads. Each Fresh retains its own clock.
Clock regression fails closed as ClockRegressed and withholds the value. Staleness returns
StaleValue[A] for explicit recovery; its toString omits the value.
A successful read is a snapshot. Scala cannot prevent the returned value from being retained after
the TTL, so this port intentionally omits Live/withLive.
These APIs are not linear or move-only. Values and Certain[A] references can be reused. The
library controls construction and value binding; it does not promise one-time authorization.
The guarantees apply to ordinary, type-safe Scala calls. Casts, reflection, bytecode tools, and
unsafe techniques can subvert them. Plain Java callers are also outside the type-level contract:
Scala opaque types erase to JVM representations; Scala-private constructors and members may be
visible in Java bytecode; and Scala sealed subclass restrictions are not enforced for Java
implementations on the Java 11 target. Scala access/type conventions are not a Java security
boundary. Put a Scala facade in front of Java code and expose validated domain values.
The project compiles its own sources with warnings as errors. Downstream warning policy remains the
consumer's responsibility; use fold when three-way handling must be structurally required.
Pinned versions are Scala 3.3.8 LTS, sbt 1.12.14, MUnit 1.3.4, scalafmt 3.11.5, and sbt-ci-release 1.12.0. Bytecode targets Java 11.
sbt "root/scalafmtAll" "examples/scalafmtAll"
sbt "root/scalafmtCheckAll" "examples/scalafmtCheckAll" test doc package examples/compile
sbt "examples/runMain io.github.slepp.requisite.examples.PaymentFlow"
sbt publishLocalCompile-negative tests use scala.compiletime.testing.typeCheckErrors, assert relevant diagnostic
text, and include nearby positive controls.
See RELEASING.md for Maven Central publication.
Licensed under either Apache-2.0 or MIT, at your option.