The Scheduler Contract
This document specifies the universal scheduler contract: the operations a scheduler must offer, the lifecycle by which a composition uses one, how capabilities and versions are negotiated, the guarantees an implementation must uphold, and the points at which the contract may be extended. It is the companion to ARCHITECTURE.md, which explains why the boundary is drawn where it is.
Normative keywords (MUST / MUST NOT / SHOULD / MAY) are used in the sense of FORGE-100 Part 2.
1. The parties
- The domain owns the vocabulary: the objects a scheduler produces are the
Forge HPC domain objects, defined in
domainand nowhere else. - A scheduler is an implementation of
SchedulerContract. It is an observer of reality that reports what it sees, translated into domain objects. It never defines a new object, field, or state. - A consumer is any code that reads through a scheduler — a downstream capability, a projection, a test. A consumer names what it requires and negotiates for it.
- An adapter (see ARCHITECTURE.md §4) is the code that knows a real source system and translates its observations into domain objects. The contract does not know adapters exist except through the separate adapter interfaces.
2. Self-description: identity, version, capabilities, manifest
Before any observation, a scheduler declares itself through a
SchedulerManifest, composed of:
SchedulerIdentity— a stable, scheduler-neutralname, with an optional display name and description. The name is opaque: the contract requires only that it exists and is stable.SchedulerVersion— the version the scheduling system reports for itself, recorded verbatim. The contract never parses or compares it; interpreting a source’s own versioning scheme is adapter knowledge.ContractVersion— the version of this contract the scheduler speaks (see §5).CapabilitySet— the capabilities the scheduler offers (see §3).
Manifest invariants (MUST):
- A scheduler MUST offer at least one capability.
- If a scheduler offers
INCREMENTAL_UPDATESit MUST also offerSNAPSHOTS: a delta is meaningless without a baseline to apply it to. manifest()MUST be deterministic — repeated calls return equal manifests.
3. Capabilities and the operations they gate
A capability names one thing a scheduler can be asked to produce. The
ratified vocabulary (SchedulerCapability) and the operation each unlocks:
| Capability | Operation | Returns |
|---|---|---|
job-discovery |
discover_jobs() |
tuple[Job, ...] |
queue-discovery |
discover_queues() |
tuple[Queue, ...] |
node-discovery |
discover_nodes() |
tuple[Node, ...] |
account-discovery |
discover_accounts() |
tuple[Account, ...] |
allocation-discovery |
discover_allocations() |
tuple[Allocation, ...] |
reservation-discovery |
discover_reservations() |
tuple[Reservation, ...] |
software-discovery |
discover_software() |
tuple[SoftwarePackage, ...] |
storage-discovery |
discover_storage() |
tuple[StorageResource, ...] |
telemetry-discovery |
discover_telemetry() |
tuple[Metric, ...] |
event-observation |
observe_events(window) |
tuple[SchedulerEvent, ...] |
snapshots |
snapshot() |
SchedulerSnapshot |
incremental-updates |
incremental_update(basis) |
IncrementalUpdate |
Rules:
manifest()andhealth()are mandatory for every scheduler and are not gated.- Every other operation is gated: a scheduler MUST implement exactly the
operations its manifest declares. Invoking a gated operation the scheduler
does not offer MUST raise
UnsupportedOperation(the base class does this by default) — never return a plausible empty result, which would be a silent lie about reality. - A scheduler MUST NOT implement an operation it does not declare (no hidden behaviour), and MUST NOT declare a capability it does not implement. Both are caught by compliance verification (§7).
CapabilitySet deduplicates and canonically orders its members, so two sets
with the same capabilities are equal and serialize identically regardless of
construction order.
4. Observation shapes
Every operation returns domain objects, either directly (the discovery operations) or wrapped in an observation shape:
ObservationWindow— a bounded, strictly positive span of time, the argument toobserve_events. Event history is exposed as a windowed query so it remains a pure observation; continuous delivery on reality’s clock is a service concern outside this contract.ObservationCursor— an opaque position in a scheduler’s stream of change. The contract never interprets it: consumers hold cursors and hand them back. A cursor carries an address, never facts.ObservedState— the domain objects one observation reported, by kind. Collections are normalized to canonically ordered tuples with unique identities, so two observations of the same reality are byte-identical however the implementation ordered them. An absent kind is an empty tuple.SchedulerSnapshot— everything the scheduler can see, at one stated moment (taken_atis mandatory: a snapshot without its time is a claim without its age).IncrementalUpdate— what changed since abasiscursor: the new state of changed or appeared objects (changed), and the objects that stopped being visible (ended). A cursor that did not advance MUST NOT carry change.SchedulerEvent(from the domain) — what happened, in time order, inside a window.
Snapshots carry state; telemetry (discover_telemetry) is separate because
measures are observations about objects; events are separate because history
is not state. This mirrors the object / measure / event triad of FORGE-002.
5. Version negotiation
The contract is versioned major.minor, additive-by-default:
- Same major means compatible vocabulary.
- A higher minor is a superset of a lower one.
ContractVersion.satisfies(required)is true exactly when the majors match and the offered minor is at least the required minor.
negotiate_contract_version(name, required=…, offered=…) returns the agreed
version (the required one) when the offer satisfies it, and raises
VersionNegotiationError — carrying both rendered versions — when it does not.
There is no silent downgrade. The version this package defines is
CONTRACT_VERSION (currently 1.0).
6. Capability negotiation
negotiate_capabilities(name, offered, required=…, desired=…):
- Required capabilities that are not offered raise
CapabilityNegotiationError, whosemissingfield names the complete, sorted set of what was absent — one failure reports the whole gap. - Desired capabilities are optional: those the scheduler offers are granted;
those it does not are returned in
CapabilityAgreement.unavailable, an unremarkable fact rather than an error. - The result is a
CapabilityAgreementwhosegrantedandunavailablesets are disjoint and deterministically ordered.
Negotiation is a pure function performed before any observation, so a composition learns whether a scheduler can serve it without touching reality.
7. Compliance and deterministic behaviour
verify_contract_compliance(scheduler) returns a deterministic tuple of
ContractViolation (empty means compliant). It is static: it reads the
manifest and inspects which operations are overridden, and never invokes an
observation operation, so it runs anywhere without any external system. It
verifies:
- the object implements
SchedulerContract(implements-contract); manifest()is constructible, typed, and deterministic (manifest-constructible,manifest-typed,manifest-deterministic);- the declared contract version is within this package’s major and not ahead of
its minor (
contract-version-major,contract-version-minor); - every declared capability is implemented
(
declared-capability-implemented); - every implemented operation is declared
(
implemented-capability-declared).
Deterministic-behaviour requirements on implementations (MUST):
manifest()is deterministic (checked).- Given the same underlying observations, an operation returns equal domain objects — everything downstream is a pure function of what crosses this boundary, so a scheduler that varies its output for equal reality breaks that purity.
- Observation shapes canonicalize their own collections, so ordering is never a source of spurious difference.
8. Lifecycle
A composition uses a scheduler through a fixed lifecycle. No step performs a write to reality — a scheduler only ever observes.
DECLARE the scheduler exposes its manifest (identity, versions,
│ capabilities). Pure declaration; nothing is observed.
▼
VERIFY verify_contract_compliance(scheduler) is empty. A
│ non-compliant scheduler is refused before use.
▼
NEGOTIATE negotiate_contract_version(...) and negotiate_capabilities(...)
│ agree, or fail loudly. The consumer now knows exactly what it
│ may ask for.
▼
CHECK health() reports observability, timestamped. A DEGRADED or
│ UNAVAILABLE report states what is wrong.
▼
OBSERVE the granted operations are invoked — discovery, events,
│ snapshot, incremental_update — each returning domain objects.
│ Repeated as the composition's cadence requires.
▼
(steady state: re-check health, re-observe; a broken source surfaces as an
honest health report and an UnsupportedOperation is never silently swallowed)
9. Extension points
The contract grows by amendment, never by local invention (a capability only one deployment understands is a leak, not a capability). The sanctioned extensions:
- A new capability. Add a
SchedulerCapabilitymember, its gated operation onSchedulerContract, its entry inOPERATION_CAPABILITIES, and bumpCONTRACT_VERSION’s minor. Existing schedulers that do not offer it are unaffected — additive-by-default (§5) makes the new minor a superset. - A new observation field. Add an optional field to an observation shape; absent means unobserved. Optional-and-defaulted keeps existing consumers working.
- A new object kind in observations. When the domain adds an object,
ObservedStateandObservationKindgain a corresponding collection and member. This follows a domain amendment, never precedes one.
Each extension is a contract minor bump recorded against this package. A breaking change (removing or re-typing anything) is a major bump and a constitutional act. Instances and adapters never extend the contract; they implement or consume the version they pin.