Skip to content

Repository files navigation

requisite for Scala 3

requisite carries provenance and workflow state in types:

  • external input becomes Trusted only 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.

Scope

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.

Install

libraryDependencies += "io.github.slepp" %% "requisite" % "0.1.0"

For local development, run sbt publishLocal.

Trust transitions

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.

Confidence gates

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.

Monotonic freshness

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.

Enforcement limits

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.

Development

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 publishLocal

Compile-negative tests use scala.compiletime.testing.typeCheckErrors, assert relevant diagnostic text, and include nearby positive controls.

See RELEASING.md for Maven Central publication.

License

Licensed under either Apache-2.0 or MIT, at your option.

About

Typed validated values, explicit trust transitions, exhaustive confidence levels, and per-value TTL freshness for Scala 3.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages