cal

package
v0.16.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package cal is the xolu scheduling primitive: a temporal-occupancy index over bookings against calendars.

The two-layer model (H1/H3)

A calendar's authoritative state is a relational booking record in SQLite (H1) — the source of truth. The structures in this package are a *derived, rebuildable bitmap index* over those bookings (H3): a per-(calendar, plane, day) occupancy raster that makes "is this span free?" and the N-way "when are all these free?" answerable by bitwise operations. The index is never authoritative; it can always be discarded and rebuilt from the SQLite records, and the invariant `index == rebuild` is the acceptance gate for every stateful operation built on top of this layer.

The conversion invariant (the load-bearing fact)

Occupancy is stored at a single fine grain (5-minute quanta). Coarser views (the rollup pyramid) are *up-conversions* of the fine layer and are therefore lossy: a coarse bucket summarises many fine quanta. This asymmetry is the reason the rollup can only ever PRUNE a match (prove one impossible), never CONFIRM one (prove one exists) — confirmation always drops to the fine grain. Reconciliation between grains always moves toward the finer grain; the toward-coarser direction loses information.

This file (Stage 0)

Settled design inputs pinned as constants, plus the core value types. No behaviour beyond construction lives here; the pure bit layer is in codec.go. The package deliberately knows nothing of time zones, recurrence, or DST — all timestamps are absolute UTC instants (see pkg/xolutime); calendar intentions live above this primitive.

Index

Constants

View Source
const (
	// QBaseSeconds is the base quantum: 5 minutes, the evidenced finest grain.
	// Settled for v1; re-basing to a finer grain is an open question (codec §6.1)
	// but never coarser.
	QBaseSeconds = 300

	// QuantaPerDay is the number of 5-minute quanta in a UTC day: 86400/300 = 288.
	QuantaPerDay = 288

	// WordsPerDay is the uint64 count holding QuantaPerDay bits: ceil(288/64) = 5.
	// Bits 288..319 (the slack in the 5th word) are unused and always zero.
	WordsPerDay = 5

	// NsPerQuantum and NsPerDay are the nanosecond spans. NOTE: neither is a power
	// of two, so flooring to a day or quantum boundary is integer division
	// (div/mul), never a bitmask — a mask would floor to the nearest 2^k ns
	// (~19.5h for a "day"), which is wrong. See codec.go dayFloorNanos.
	NsPerQuantum int64 = QBaseSeconds * 1_000_000_000 // 300_000_000_000
	NsPerDay     int64 = 86_400 * 1_000_000_000       // 86_400_000_000_000
)
View Source
const (
	// DaypartHours is the v1 rollup granularity: one 3-hour daypart.
	DaypartHours = 3

	// QuantaPerDaypart is 3h of 5-minute quanta: (3*3600)/300 = 36.
	QuantaPerDaypart = (DaypartHours * 3600) / QBaseSeconds // 36

	// DaypartsPerDay is the number of dayparts in a day: 288/36 = 8.
	// One rollup byte (8 bits) summarises a whole day at daypart granularity.
	DaypartsPerDay = QuantaPerDay / QuantaPerDaypart // 8
)
View Source
const (
	EntityNil       uint64 = 0x0000000000000000 // floor: no binding by design
	EntityMaxValid  uint64 = 0xFFFFFFFFFFFFFF00 // allocator ceiling; top 256 reserved
	EntityTombstone uint64 = 0xFFFFFFFFFFFFFFFF // was bound, target entity deleted

)
View Source
const (

	// KeySize is the fixed occupancy-key width: 1+4+1+8.
	KeySize = 1 + 4 + 1 + 8 // 14
)

Variables

View Source
var ErrBearerRequired = errors.New("cal: binding booking requires a live bearer")

ErrBearerRequired reports that a binding booking was requested without a live bearer entity handle.

View Source
var ErrCalendarExists = errors.New("cal: calendar already exists")

ErrCalendarExists reports that CreateCalendar was called with a calendar_id already present in the tenant.

View Source
var ErrIllegalTransition = errors.New("cal: illegal lifecycle transition")

ErrIllegalTransition reports that a lifecycle transition (confirm, decline, complete, cancel, mark-missed) is not permitted from the booking's current state per the A9 rules.

View Source
var ErrInvalidSpan = errors.New("cal: invalid span")

ErrInvalidSpan reports that a span carries Start >= End, or that either instant is zero.

View Source
var ErrModeNotSupported = errors.New("cal: mode not supported (only exclusive)")

ErrModeNotSupported reports that a booking was submitted with a mode outside the exclusive-only vocabulary. Introduced in v0.14.12 when ModeShared and ModeSubPrefix were removed from the type surface (see Mode godoc in pkg/cal/booking.go for the rationale).

View Source
var ErrUnknownBooking = errors.New("cal: unknown booking")

ErrUnknownBooking reports that a request references a booking_id that does not exist on the named calendar.

View Source
var ErrUnknownCalendar = errors.New("cal: unknown calendar")

ErrUnknownCalendar reports that a request references a calendar_id that does not exist in the current tenant scope.

Functions

func BusyDayparts

func BusyDayparts(rollups ...DaypartRollup) uint8

BusyDayparts returns the dayparts that are occupied in ANY of the given rollups (the OR aggregate unioned) — prunes availability?q=busy: a daypart with no occupancy in any calendar is definitely free at fine grain too.

func CandidateDaypartIndices

func CandidateDaypartIndices(rollups ...DaypartRollup) []int

CandidateDaypartIndices returns the indices (0..7) of dayparts surviving match-pruning. Convenience over MatchCandidateDayparts.

func DayKey

func DayKey(ord CalOrdinal, plane Plane, t ot.Instant) []byte

DayKey is the convenience encoder for an arbitrary instant: it floors to the instant's UTC day and encodes. This is the function write/read paths use.

func DecodeKey

func DecodeKey(key []byte) (ord CalOrdinal, plane Plane, dayNanos int64, err error)

DecodeKey reverses EncodeKey. It returns the calendar ordinal, plane, and the day_unixnano (day-floored UnixNano, UTC).

func EncodeKey

func EncodeKey(ord CalOrdinal, plane Plane, dayNanos int64) []byte

EncodeKey builds the fixed-width occupancy key for a (calendar, plane, day). dayNanos must already be day-floored (use DayKey for the common path, which floors for you).

func IsSealed

func IsSealed(err error) bool

IsSealed reports whether err is a SealedError.

func MatchCandidateDayparts

func MatchCandidateDayparts(rollups ...DaypartRollup) uint8

MatchCandidateDayparts returns the dayparts that survive match-pruning across the given calendars' rollups, as a bitmask (bit d set => daypart d must be confirmed at the fine grain).

The ONLY sound prune for "is there a commonly-free fine quantum in this daypart" is: if ANY single calendar has the daypart entirely busy (no free quantum — its AnyClear bit is clear), then no common free slot can exist there, so the daypart is pruned. A daypart survives iff every calendar has at least one free quantum in it. Survival is necessary but NOT sufficient (the free quanta may not align) — hence prune-not-confirm: surviving dayparts MUST be verified against the fine layer.

func ValidEntity

func ValidEntity(h uint64) bool

ValidEntity reports whether h is an assignable real handle (not a sentinel).

Types

type Booking

type Booking struct {
	BookingID   string
	CalendarID  string
	State       State
	Span        Span       // [Start, End) UTC
	Mode        Mode       // exclusive (the only valid value; see Mode godoc)
	Bearer      uint64     // entity handle (A10); required live for binding
	BufferAfter ot.Instant // optional: end+buffer; zero Instant = no buffer
	CreatedAt   ot.Instant
	UpdatedAt   ot.Instant
	DetailRef   string // optional ref into the meta/detail document
}

Booking is the authoritative booking record (H1). Times are absolute UTC instants (xolutime); the originating wall-clock intention is NOT stored here — it is the caller's, per R-T1.

type BookingSource

type BookingSource interface {
	// Calendars returns every calendar definition.
	Calendars() []Calendar
	// LiveBookings returns every booking in a state that occupies a plane
	// (proposed/binding/honoured); terminal-state bookings are excluded.
	LiveBookings() []Booking
}

BookingSource provides the live bookings and calendars the index derives from. Stage 3 implements it in memory (MemBookingSource); Stage 3's SQLite wiring will provide a Pebble/SQLite-backed implementation behind the same interface, so Rebuild and the invariant test are storage-agnostic.

type CalOrdinal

type CalOrdinal uint32

CalOrdinal is the per-tenant dense calendar identifier (codec §3.2), assigned at cal/def time and recorded in the SQLite calendar record. uint32 (the one width the codec widens over ts's uint16 timeline id).

type Calendar

type Calendar struct {
	CalendarID   string
	Ordinal      CalOrdinal // dense per-tenant key-space coordinate (codec §3.2)
	EntityRef    uint64     // the one entity this calendar tracks (§1)
	DefaultState State      // proposed | binding (§1)
	MatchPolicy  MatchConsiders
}

Calendar is the calendar definition record (the policy fields the substrate would otherwise have to guess, cal-rest-api.md §1, plus GATE-1's match policy).

Cal implements exclusive-only occupancy (see Mode godoc). The Capacity field that existed prior to v0.14.13 has been removed: it was descriptive metadata only, the occupancy engine never honoured N > 1, and the design review with Google Calendar's model confirmed exclusive-only as cal's target. Callers that need "how many humans fit in this room" metadata should carry it on a separate entity record, not on the calendar itself.

type Capacity

type Capacity struct {
	State    CapacityState `json:"state"`
	Capacity int           `json:"capacity"` // 100 − binding%
	Counts   Counts        `json:"counts"`
}

Capacity is the full q=capacity result: the ternary state, the scalar capacity = 100 − confirmed% (binding share; proposals ignored), and the raw counts.

type CapacityState

type CapacityState string

CapacityState is the q=capacity ternary (§3b).

const (
	StateFree CapacityState = "free" // no commitments at all
	StateIdk  CapacityState = "idk"  // proposed present, no binding (uncertain)
	StateBusy CapacityState = "busy" // binding present
)

type CheckResult

type CheckResult struct {
	Feasible        bool
	NearestOpenings []Span // when infeasible: where "yes" lives nearby
}

CheckResult is the outcome of a feasibility check: feasible, or the conflicts and nearest openings.

type CommitMember

type CommitMember struct {
	CalendarID string
	BookingID  string
	Mode       Mode
	Bearer     uint64 // required live for a binding placement
}

CommitMember is one calendar's part of a cross-calendar placement.

type CommitResult

type CommitResult struct {
	Committed bool
	Placed    []string // booking ids placed (only when Committed)
	Blocking  []string // calendars that blocked (only when not Committed)
	Conflicts []Conflict
}

CommitResult reports the outcome. On success Committed is true and Placed lists the booking ids. On conflict Committed is false, nothing was written, and Blocking names the calendars whose occupancy prevented placement.

type Conflict

type Conflict struct {
	With   string // booking id clashed with
	Over   Span   // the overlapping region
	Reason string // fixed enum (exclusive-vs-exclusive overlap, etc.)
}

Conflict is one entry of the universal conflict report (§6), shared by create, check, and move.

type Counts

type Counts struct {
	Binding  int
	Proposed int
	Free     int
}

Counts holds quantum tallies over a period. Binding = quanta with a binding commitment; Proposed = quanta proposed-but-not-binding (proposed set, binding clear); Free = quanta with no commitment of any kind. The three partition the period's quanta: Binding + Proposed + Free == total quanta in the period.

func (Counts) Total

func (c Counts) Total() int

Total is the number of quanta in the period.

type DayBitmap

type DayBitmap [WordsPerDay]uint64

DayBitmap is one day's occupancy raster on one plane: 288 bits in 5 uint64 words. Bit q (0..287) set means quantum q of that day is occupied; q = seconds_into_day / 300. Bits 288..319 are unused and must stay zero.

func AndFree

func AndFree(days ...DayBitmap) DayBitmap

AndFree returns the day's worth of commonly-FREE quanta across all the given busy-bitmaps: a bit is set in the result iff that quantum is free in EVERY input. This is the N-way match kernel (F16): "when are all these calendars simultaneously free?" Implemented as the AND of each input's free-mask.

With no inputs the result is the all-free day (every valid quantum set), which is the correct identity for an empty intersection.

func (DayBitmap) And

func (b DayBitmap) And(o DayBitmap) DayBitmap

And returns the bitwise intersection of two days (occupied in BOTH). This is the per-day kernel of the N-way match: a quantum free in the result of ANDing "busy" maps is... not what we want — match operates on FREE maps. See AndFree below, which is the matching primitive. And is the raw busy-AND, exposed for completeness and testing.

func (*DayBitmap) Clear

func (b *DayBitmap) Clear(q int)

Clear marks quantum q free.

func (DayBitmap) IsZero

func (b DayBitmap) IsZero() bool

IsZero reports whether the day has no occupancy (the sparse-store "no value" equivalent — an all-zero day need not be stored at all).

func (DayBitmap) Or

func (b DayBitmap) Or(o DayBitmap) DayBitmap

Or returns the bitwise union (occupied in EITHER) — e.g. combining binding and proposed planes when a caller asks for both (the GATE-1 binding+proposed case; the bit layer provides the operation, the policy decision lives above).

func (DayBitmap) PopCount

func (b DayBitmap) PopCount() int

PopCount returns the number of occupied quanta in the day.

func (*DayBitmap) Set

func (b *DayBitmap) Set(q int)

Set marks quantum q (0..287) occupied. Out-of-range q is a no-op guard.

func (*DayBitmap) SetRange

func (b *DayBitmap) SetRange(lo, hi int)

SetRange marks quanta [lo, hi) occupied (half-open), clamped to [0, 288).

func (DayBitmap) Test

func (b DayBitmap) Test(q int) bool

Test reports whether quantum q is occupied.

type DayOccupancy

type DayOccupancy struct {
	DayNanos int64
	Bits     DayBitmap
}

DayOccupancy is one day's contribution from a span: the day key's floored UnixNano and the bits that day should have set.

func SpanDays

func SpanDays(s Span) ([]DayOccupancy, error)

SpanDays expands a half-open [Start, End) span into per-day occupancy. Returns an error for an invalid span. A span ending exactly on a midnight contributes nothing to that midnight's day (half-open: the end quantum is exclusive).

type DaypartRollup

type DaypartRollup struct {
	Or       uint8 // daypart has ≥1 occupied quantum
	AnyClear uint8 // daypart has ≥1 free quantum
}

DaypartRollup is one day's rollup: two 8-bit masks, one bit per 3-hour daypart. Or bit d set => some quantum in daypart d is occupied. AnyClear bit d set => some quantum in daypart d is free.

func RollupDay

func RollupDay(b DayBitmap) DaypartRollup

RollupDay computes both daypart aggregates of a fine day bitmap.

type IndexStore

type IndexStore struct {

	// Fault-injection hooks (T-31, v0.14.14). Non-nil hooks are called
	// at the top of the corresponding mutation and, if they return a
	// non-nil error, the mutation aborts with that error before any
	// bitmap change is persisted. Nil hooks are the normal path.
	//
	// Intended for tests exercising the SQL/index disagreement recovery
	// path — see cal_fault_injection_test.go. Not part of the public
	// API contract; the fields are exported only so tests in the same
	// package can set them without a reflection dance.
	AddToPlaneFaultHook      func(b Booking) error
	RemoveFromPlaneFaultHook func(b Booking, plane Plane) error
	// contains filtered or unexported fields
}

IndexStore is the Pebble-backed occupancy index (the derived bitmap, H3). It is NOT authoritative: it is a pure function of the live booking records (H1), and can be discarded and rebuilt at any time (Rebuild). The booking records themselves live in the tenant's primary store (SQLite); this package's Stage 3 works against an in-memory BookingSource for them, so the index logic and the `index == rebuild` invariant can be exercised before the SQLite wiring lands.

Storage layout mirrors ts: a Pebble db under <calDir>/db. Keys are the 14-byte occupancy keys (codec §3.2); values are 40-byte day bitmaps. Only days with occupancy have a value (sparse). The store opens at the directory returned by storelayout.TenantCalDir(base, tenantID).

func OpenIndexStore

func OpenIndexStore(dir string) (*IndexStore, error)

OpenIndexStore opens (creating if needed) the cal occupancy index at dir. dir is typically storelayout.TenantCalDir(base, tenantID); the Pebble database lives in dir/db, matching the ts convention.

func (*IndexStore) AddOccupancy

func (s *IndexStore) AddOccupancy(b Booking) error

AddOccupancy is the incremental write path: register a newly-created or state-changed booking's occupancy into the index. For a create or a confirm (which adds to a plane) this is an OR. For a state change that MOVES a booking between planes (proposed->binding on confirm) or REMOVES it (cancel), the caller must remove the old contribution too; the always-correct path for any removal is RebuildFrom, since overlapping bookings make incremental clear unsafe. Stage 5 (lifecycle) refines this; Stage 3 uses AddOccupancy for create and RebuildFrom as the removal/correctness path.

func (*IndexStore) Check

func (s *IndexStore) Check(c Calendar, span Span, mode Mode) (CheckResult, error)

Check reports whether a booking of the given span/mode would be feasible on a calendar right now, writing nothing. Same occupancy engine as a real create, so check and create cannot drift. For an exclusive booking, feasible means the span is entirely free of any commitment (binding or proposed) on the calendar.

When infeasible, NearestOpenings offers free spans of the same duration within a search window around the requested time (the "here's where yes lives" hint).

func (*IndexStore) Close

func (s *IndexStore) Close() error

Close closes the underlying Pebble database.

func (*IndexStore) Match

func (s *IndexStore) Match(cals []Calendar, from, to ot.Instant, duration time.Duration) (MatchResult, error)

Match returns spans within [from,to) where every named calendar is simultaneously free for at least `duration`, honouring each calendar's match_considers policy. cals must be resolved Calendar records (the caller looks them up; Match needs their ordinals and policies).

The day-level rollup prunes days that cannot contain a coincidence before the fine AND runs (prune-not-confirm: a pruned day definitely has none; a surviving day is confirmed at fine grain). When the result is empty, Blocking names the calendars responsible.

func (*IndexStore) ReadOccupancy

func (s *IndexStore) ReadOccupancy(calendarID string) (*Occupancy, error)

ReadOccupancy loads the index into an in-memory Occupancy window for a single calendar, so the Stage-2 availability reads run against persisted data. It scans the calendar's occupancy keys on both planes.

func (*IndexStore) RebuildFrom

func (s *IndexStore) RebuildFrom(src BookingSource) error

RebuildFrom discards the entire occupancy keyspace and reconstructs it from the source's live bookings. This is both the recovery path and the test oracle for the index == rebuild invariant.

func (*IndexStore) RegisterCalendar

func (s *IndexStore) RegisterCalendar(c Calendar)

RegisterCalendar records a calendar's ordinal in the in-memory map.

type Lifecycle

type Lifecycle struct {
	// contains filtered or unexported fields
}

Lifecycle wires booking-state transitions to both the authoritative record (via the source) and the derived index (incrementally). The source is the H1 truth; the index is kept in step on each transition.

func NewLifecycle

func NewLifecycle(src Store, index *IndexStore) *Lifecycle

NewLifecycle binds a source and an index.

func (*Lifecycle) Cancel

func (l *Lifecycle) Cancel(calendarID, bookingID string) error

Cancel: proposed|binding -> cancelled.

func (*Lifecycle) Complete

func (l *Lifecycle) Complete(calendarID, bookingID string) error

Complete: binding -> honoured (same plane).

func (*Lifecycle) Confirm

func (l *Lifecycle) Confirm(calendarID, bookingID string) error

Confirm: proposed -> binding (the cross-plane move).

func (*Lifecycle) Create

func (l *Lifecycle) Create(b Booking) (Booking, error)

Create inserts a new booking in the calendar's default state and adds its occupancy to the index. Returns the created booking.

func (*Lifecycle) Decline

func (l *Lifecycle) Decline(calendarID, bookingID string) error

Decline: proposed -> not-committed.

func (*Lifecycle) MarkMissed

func (l *Lifecycle) MarkMissed(calendarID, bookingID string) error

MarkMissed: binding -> missed (the sweeper-written non-occurrence, §7). Exposed for the reconciliation sweeper; not a user endpoint.

func (*Lifecycle) MatchCommit

func (l *Lifecycle) MatchCommit(when Span, members []CommitMember) (CommitResult, error)

MatchCommit places one booking per calendar at the given span, atomically. Each member's booking lands in its calendar's default state (proposed or binding). All N land or none do.

func (*Lifecycle) Move

func (l *Lifecycle) Move(calendarID, bookingID string, to Span) (MoveResult, error)

Move atomically reschedules a booking to a new span. It re-runs placement at the destination: if the destination is infeasible the booking is left exactly where it was and Moved is false with the conflict report. On success the booking occupies the new span and the prior span is gone from the index.

move never cascades to dependents; if any exist they are reported as stranded. (Dependency tracking is not in v1's record, so StrandedDependents is always empty here; the field exists so the contract is visible and stable.)

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager is the per-tenant assembly point for cal: it binds each tenant's SQLite booking source (over the tenant's primary store DB) to its Pebble occupancy index (at the storelayout cal path) and a lifecycle engine over the pair. The REST handlers sit on the Manager: one CalFor(tenant) call yields a ready lifecycle.

Assemblies are built lazily and cached per tenant (CalFor is idempotent), the same model the server uses for per-tenant sulpher job managers. The Manager does not own the *sql.DB (the caller's store does); it owns the index stores it opens, which Close releases.

func NewManager

func NewManager(baseDir string, db *sql.DB) *Manager

NewManager binds a base directory (under which per-tenant cal index stores are opened at storelayout.TenantCalDir) and the tenant primary-store *sql.DB. The ordinal-reuse policy defaults to retire (false).

func (*Manager) CalFor

func (m *Manager) CalFor(tenantID uint16) (*Lifecycle, error)

CalFor returns the lifecycle engine for a tenant, assembling it on first use. Idempotent: repeated calls for the same tenant return the same lifecycle.

func (*Manager) Close

func (m *Manager) Close() error

Close releases every assembled tenant's index store. The *sql.DB is owned by the caller's store and is NOT closed here.

func (*Manager) CreateCalendar

func (m *Manager) CreateCalendar(tenantID uint16, c Calendar) (Calendar, error)

CreateCalendar creates a calendar in the given tenant's persistence layer AND registers it with the in-memory index. This is the transactional operation callers should almost always use rather than composing SourceFor(t).CreateCalendar(c) with IndexFor(t).RegisterCalendar(c) themselves.

Without the index registration step, subsequent Lifecycle.Create calls against the calendar fail with ErrUnknownCalendar from IndexStore.ordinalFor — the index rebuild only runs at first assemble(), so any calendar added after that point is invisible to the ordinal map until explicitly registered. This facade eliminates that footgun.

Rollback semantics: the SQL insert runs first. If it fails, no index change occurs. If it succeeds, the index register cannot fail (it is a pure in-memory map update). Consequently the observable state after this method returns is either "both persisted and indexed" (nil error) or "neither" (non-nil error, with sentinel wrapping via the source layer).

Concurrency: the index register runs under IndexStore's own mutex. The SQL insert runs under SQLite's transaction semantics. Callers concurrently creating the same calendar will see one succeed and the others fail with ErrCalendarExists (wrapped by the source layer).

Introduced in v0.14.10.

func (*Manager) IndexFor

func (m *Manager) IndexFor(tenantID uint16) *IndexStore

IndexFor returns the tenant's occupancy index store (assembling if needed).

func (*Manager) SetOrdinalReuse

func (m *Manager) SetOrdinalReuse(reuse bool)

SetOrdinalReuse selects the OrdinalReuse policy for sources assembled after the call (GATE-3 #2). Must be set before the first CalFor for a tenant to take effect for that tenant.

func (*Manager) SourceFor

func (m *Manager) SourceFor(tenantID uint16) *SQLiteBookingSource

SourceFor returns the tenant's SQLite booking source (assembling if needed). Returns nil only if assembly fails, which CalFor surfaces with an error; use CalFor when an error path is needed.

type MatchConsiders

type MatchConsiders string

MatchConsiders is the GATE-1 per-calendar match policy.

const (
	ConsiderBinding         MatchConsiders = "binding"          // optimistic: proposals don't block
	ConsiderBindingProposed MatchConsiders = "binding+proposed" // pessimistic: proposals block
)

type MatchResult

type MatchResult struct {
	Matches  []Span
	Checked  []string
	Blocking []string // calendars with zero free coincidence (only set when Matches empty)
}

MatchResult is the outcome of a match: coincident free spans, or the calendars that blocked any coincidence.

type MemBookingSource

type MemBookingSource struct {
	// contains filtered or unexported fields
}

MemBookingSource is an in-memory implementation of BookingSource: the authoritative booking + calendar records (H1) held in maps. Stage 3 uses it to exercise the index write path and the index == rebuild invariant before the SQLite wiring lands. The SQLite-backed source will implement the same BookingSource interface, so Rebuild and the invariant test are unchanged.

It also owns the dense per-tenant cal_ordinal counter (GATE-3 #5): a uint32 allocated ascending from 1. OrdinalReuse policy governs whether a deleted calendar's ordinal returns to the pool; the default is retire (counter only moves up).

func NewMemBookingSource

func NewMemBookingSource(reuse bool) *MemBookingSource

NewMemBookingSource returns an empty source. reuse selects the OrdinalReuse policy (false = retire, the safe default; true = reuse retired ordinals).

func (*MemBookingSource) Calendars

func (m *MemBookingSource) Calendars() []Calendar

Calendars implements BookingSource.

func (*MemBookingSource) CreateCalendar

func (m *MemBookingSource) CreateCalendar(c Calendar) (Calendar, error)

CreateCalendar registers a calendar, allocating its dense ordinal. The caller supplies policy fields; Ordinal is assigned here and returned in the stored record.

func (*MemBookingSource) DeleteCalendar

func (m *MemBookingSource) DeleteCalendar(calendarID string) error

DeleteCalendar removes a calendar and (under reuse policy) returns its ordinal to the pool. Its bookings must already be gone.

func (*MemBookingSource) LiveBookings

func (m *MemBookingSource) LiveBookings() []Booking

LiveBookings implements BookingSource: only plane-occupying states.

func (*MemBookingSource) LiveBookingsOn

func (m *MemBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking

LiveBookingsOn implements PlaneBookingSource: live bookings for one calendar that occupy the given plane.

func (*MemBookingSource) PutBooking

func (m *MemBookingSource) PutBooking(b Booking) error

PutBooking inserts or updates a booking record (the authoritative H1 write).

func (*MemBookingSource) SetStateFrom

func (m *MemBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error

SetStateFrom transitions a booking to a new state (the A9 lifecycle write) if and only if it is still in the expected from-state — the compare half of the T-34 compare-and-swap, evaluated under the source lock so concurrent racers serialise here.

type Mode

type Mode string

Mode is the exclusivity vocabulary. Cal implements exclusive-only resource semantics matching Google Calendar's room model: a booking takes the whole calendar for its span, and any overlapping booking is a conflict.

The wire vocabulary previously included ModeShared ("consumes one unit of capacity") and ModeSubPrefix ("sub:<child_id>") as reserved future extensions for pooled and sub-resource semantics. Both were removed in v0.14.12 after a design review compared cal against Google Calendar's actual model (rooms are boolean; capacity is descriptive metadata for filtering, not a booking-concurrency limit) and confirmed cal's target is the same shape. Implementing pooled resources properly would require a counter-based bitmap encoding with ~8x storage growth and no user-facing feature that Google Calendar itself provides; the vocabulary was removed rather than left as accepted-but-inert.

const (
	ModeExclusive Mode = "exclusive" // whole resource, the only valid value
)

type MoveResult

type MoveResult struct {
	Moved              bool
	Conflicts          []Conflict
	StrandedDependents []string // dependents left behind (reported, never cascaded)
}

MoveResult reports the outcome of a move. On conflict the booking is untouched and Moved is false.

type Objective

type Objective string

Objective is the fixed openings ordering enum.

const (
	ObjEarliest   Objective = "earliest"  // chronological
	ObjFirstFit   Objective = "first-fit" // first hole that fits (== earliest, one result)
	ObjEmptiest   Objective = "emptiest"  // most surrounding free margin first
	ObjLongestClr Objective = "longest-clear-margin"
)

type Occupancy

type Occupancy struct {
	// contains filtered or unexported fields
}

Occupancy is an in-memory occupancy window for one calendar: sparse per-day bitmaps on each plane, keyed by day-floored UnixNano. Only days with occupancy are present (the sparse-store model). This is the Stage-2 stand-in for the persisted index; Stage 3 replaces the maps with Pebble-backed storage behind the same read methods.

func NewOccupancy

func NewOccupancy() *Occupancy

NewOccupancy returns an empty occupancy window.

func (*Occupancy) Add

func (o *Occupancy) Add(p Plane, s Span) error

Add ORs a span's occupancy into the given plane. Invalid spans are rejected.

func (*Occupancy) Capacity

func (o *Occupancy) Capacity(p Period) (Capacity, error)

Capacity computes q=capacity over the period.

  • state: binding present => busy; else proposed present => idk; else free.
  • capacity: 100 − (binding quanta / total quanta · 100), rounded; proposals do not reduce it.

func (*Occupancy) CountQuanta

func (o *Occupancy) CountQuanta(p Period) (Counts, error)

CountQuanta tallies binding/proposed/free quanta over the period. A quantum is Binding if its binding bit is set; else Proposed if its proposed bit is set; else Free. (Binding dominates: a quantum both binding and proposed counts as Binding — it is genuinely taken.)

func (*Occupancy) IsBusy

func (o *Occupancy) IsBusy(p Period) (bool, error)

IsBusy reports q=busy: is there ONE OR MORE commitment of any kind? The exact complement of IsFree.

func (*Occupancy) IsFree

func (o *Occupancy) IsFree(p Period) (bool, error)

IsFree reports q=free: are there ZERO commitments of any kind in the period?

func (*Occupancy) Openings

func (o *Occupancy) Openings(from, to ot.Instant, duration time.Duration, obj Objective) ([]Opening, error)

Openings returns spans wide enough for duration within [from,to), ordered per objective. Cal implements exclusive-only occupancy: every booking takes the whole calendar for its span (see Mode godoc in booking.go). Buffer-after aftermath holds are honoured; capacity sub-units are not applicable in the exclusive-only model.

type Opening

type Opening struct {
	Start  ot.Instant
	End    ot.Instant
	Margin time.Duration // length of the containing free run
}

Opening is a free span wide enough for the requested duration.

type Period

type Period struct {
	Start ot.Instant
	End   ot.Instant
}

Period is the resolved time range of an availability query. The REST layer parses human/ISO period strings ("2027/month/05") into a Period; the bit layer works in absolute instants only.

func PeriodDay

func PeriodDay(t ot.Instant) Period

PeriodDay builds a one-UTC-day period for the day containing t.

func (Period) Valid

func (p Period) Valid() bool

Valid reports a well-formed period.

type Plane

type Plane uint8

Plane selects the binding (confirmed) or proposed (tentative) occupancy plane. The two planes are stored under distinct keys; their semantics in matching are governed by GATE-1 (codec §6.5) and are not decided in the bit layer.

const (
	PlaneBinding  Plane = 0x00 // confirmed bookings
	PlaneProposed Plane = 0x01 // tentative (proposed-but-not-binding) bookings
)

func (Plane) String

func (p Plane) String() string

func (Plane) Valid

func (p Plane) Valid() bool

Valid reports whether p is a defined plane.

type PlaneBookingSource

type PlaneBookingSource interface {
	BookingSource
	// LiveBookingsOn returns live bookings for calendarID whose state occupies
	// the given plane.
	LiveBookingsOn(calendarID string, plane Plane) []Booking
}

PlaneBookingSource extends BookingSource with the scoped query the incremental maintenance needs: the live bookings on one calendar that occupy a given plane. The in-memory and (future) SQLite sources both implement it.

type SQLiteBookingSource

type SQLiteBookingSource struct {
	// contains filtered or unexported fields
}

SQLiteBookingSource is the persistent, authoritative booking record (H1), backed by the S11 cal_* tables in the tenant's primary store. It implements the same Store interface as MemBookingSource, so the lifecycle, commit, and seal logic — and every test — are unchanged behind it.

TIME DISCIPLINE (xolutime invariant, docs/TIME_HANDLING.md):

  • Every persisted timestamp column is an absolute UTC instant stored as int64 UnixNano. The boundary is exactly instantToNanos / nanosToInstant below — instant.UnixNano() (always UTC, since ot.Instant is UTC-internal) on write, ot.FromUnixNano(ns) on read. No time.Time ever crosses this boundary, and this file never calls time.Now(): "now" for created/updated is minted upstream via ot.Now() (in Lifecycle.Create) and merely serialised here, so storage is a pure serialiser with no clock access.

Tenancy follows xolu config (GATE-3 #1): the tenant_id column discriminates in shared-file mode and is constant 0 in per-file mode, identical to every other v2 table. The caller supplies the tenant id; this source does not decide it.

func NewSQLiteBookingSource

func NewSQLiteBookingSource(db *sql.DB, tenantID uint16, reuse bool) *SQLiteBookingSource

NewSQLiteBookingSource binds a source to an open *sql.DB (the tenant's primary store, with the S11 schema already migrated) and a tenant id. reuse selects the OrdinalReuse policy (false = retire, the safe default).

func (*SQLiteBookingSource) Booking

func (s *SQLiteBookingSource) Booking(calendarID, bookingID string) (Booking, bool)

Booking is the exported booking accessor: returns the persisted record for a (calendar, booking) pair. Used by callers outside the package (and tests) that need to read a booking back from the authoritative store.

func (*SQLiteBookingSource) Calendar

func (s *SQLiteBookingSource) Calendar(calendarID string) (Calendar, bool)

Calendar is the exported calendar accessor: returns the persisted calendar record for an id. Used by callers outside the package (and tests).

func (*SQLiteBookingSource) Calendars

func (s *SQLiteBookingSource) Calendars() []Calendar

Calendars implements BookingSource.

func (*SQLiteBookingSource) CreateCalendar

func (s *SQLiteBookingSource) CreateCalendar(c Calendar) (Calendar, error)

CreateCalendar inserts a calendar, allocating its dense ordinal via the cal_ord_seq monotonic allocator (the SQLite analogue of the in-memory counter; GATE-3 #5). Defaults mirror MemBookingSource.

func (*SQLiteBookingSource) DeleteCalendar

func (s *SQLiteBookingSource) DeleteCalendar(calendarID string) error

DeleteCalendar removes a calendar. Its bookings must already be gone.

func (*SQLiteBookingSource) LiveBookings

func (s *SQLiteBookingSource) LiveBookings() []Booking

LiveBookings implements BookingSource: every booking in a plane-occupying state. The state filter is pushed into SQL using the live-state set.

func (*SQLiteBookingSource) LiveBookingsOn

func (s *SQLiteBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking

LiveBookingsOn implements PlaneBookingSource: live bookings for one calendar on one plane. The plane maps to its state set; the (calendar_id, state) index serves this — the hot path for lifecycle/move/commit.

func (*SQLiteBookingSource) PutBooking

func (s *SQLiteBookingSource) PutBooking(b Booking) error

PutBooking inserts or replaces a booking record.

func (*SQLiteBookingSource) SetStateFrom

func (s *SQLiteBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error

SetStateFrom transitions a booking's state if and only if it is still in the expected from-state. The guarded UPDATE is the compare half of the T-34 compare-and-swap: under concurrent racers, SQLite serialises the writes and exactly one matches the WHERE state=? clause; the rest see zero rows affected and fail with ErrIllegalTransition.

type SealedError

type SealedError struct {
	Span     Span
	Frontier ot.Instant
}

SealedError is returned when a mutation targets a sealed (immutable past) day.

func (*SealedError) Error

func (e *SealedError) Error() string

type Sealer

type Sealer struct {
	// contains filtered or unexported fields
}

Sealer manages the now-crossing seal frontier for an IndexStore + Lifecycle. It serialises frontier advances against mutations via a single mutex, so the seal and the confirm cross-plane move can never destructively interleave.

func NewSealer

func NewSealer(lc *Lifecycle) *Sealer

NewSealer binds a sealer to a lifecycle. The frontier starts at zero (nothing sealed).

func (*Sealer) AdvanceTo

func (s *Sealer) AdvanceTo(now ot.Instant)

AdvanceTo advances the seal frontier to `now`, sealing every day whose window has fully passed. It is monotone: a request to move the frontier backward is a no-op. Advancing takes the mutex, so it cannot interleave with a guarded mutation — the seal observes only fully-applied index states.

Sealing here is a logical freeze: because the sealed past is simply never mutated again (guarded below), no per-day rewrite is required at seal time. The frontier IS the seal. (A physical freeze — copying sealed days to a cold store — is the deferred store-split, codec §6.9; v1 seals logically.)

func (*Sealer) CancelSealed

func (s *Sealer) CancelSealed(calendarID, bookingID string) error

CancelSealed runs Cancel under the seal guard.

func (*Sealer) ConfirmSealed

func (s *Sealer) ConfirmSealed(calendarID, bookingID string) error

ConfirmSealed runs Confirm under the seal guard. The confirm cross-plane move (the hazardous one) is rejected if the booking's span touches a sealed day.

func (*Sealer) CreateSealed

func (s *Sealer) CreateSealed(b Booking) (Booking, error)

CreateSealed runs Create under the seal guard: a new booking cannot be created into the sealed past.

func (*Sealer) Frontier

func (s *Sealer) Frontier() ot.Instant

Frontier returns the current seal frontier (the instant before which days are sealed). Days whose end is at or before the frontier are immutable.

func (*Sealer) MoveSealed

func (s *Sealer) MoveSealed(calendarID, bookingID string, to Span) (MoveResult, error)

MoveSealed runs Move under the seal guard, rejecting it if EITHER the current span or the destination touches a sealed day (you can move neither out of nor into the sealed past).

type Span

type Span struct {
	Start ot.Instant
	End   ot.Instant
}

Span is a concrete booking interval. Both endpoints are absolute UTC instants (xolutime.Instant); the span carries no zone or recurrence. It is half-open: [Start, End), so adjacent spans (one ending exactly where the next starts) do not overlap — matching the bitmap's quantum semantics.

func (Span) Valid

func (s Span) Valid() bool

Valid reports whether the span is well-formed: Start strictly before End. A zero-length or inverted span is invalid (a booking must occupy time).

type State

type State string

State is an A9 lifecycle state.

const (
	StateProposed     State = "proposed"      // tentative; occupies the proposed plane
	StateBinding      State = "binding"       // confirmed; occupies the binding plane
	StateHonoured     State = "honoured"      // completed; occupies the binding plane
	StateNotCommitted State = "not-committed" // declined proposal; occupies no plane
	StateCancelled    State = "cancelled"     // cancelled; occupies no plane
	StateMissed       State = "missed"        // sweeper-written non-occurrence; no plane
)

type Store

type Store interface {
	PlaneBookingSource

	CreateCalendar(c Calendar) (Calendar, error)
	DeleteCalendar(calendarID string) error
	PutBooking(b Booking) error
	SetStateFrom(calendarID, bookingID string, from, to State) error
	// contains filtered or unexported methods
}

Store is the full authoritative-record interface the Lifecycle engine drives: the calendar/booking CRUD plus the scoped queries. Both MemBookingSource and the SQLite-backed source implement it, so the lifecycle, commit, and seal logic (and every test) are storage-agnostic. It extends PlaneBookingSource (which extends BookingSource) with the mutating and lookup operations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL