planner

package
v0.62.13 Latest Latest
Warning

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

Go to latest
Published: Jun 28, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package planner is L3 of the SigComply CLI: reads the project config plus the selected framework plus the registries and produces a fully resolved RunPlan — slot bindings, effective parameters, resolved exceptions, derived period. The planner does no external I/O; planning errors exit with code 3.

See docs/architecture/02-layers.md for the full layer contract.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CadenceInterval added in v0.21.0

func CadenceInterval(cadence string) time.Duration

CadenceInterval returns the minimum elapsed time after a successful run before a cadence is due again. Intervals are deliberately shorter than their nominal values to absorb CI cron drift: GitHub Actions and GitLab CI commonly fire scheduled jobs 5–15 minutes late under load, and a strict 24h gate would silently skip days when drift crosses midnight. A 1h slack per cadence is enough for both platforms in practice.

continuous and hourly return 0 — the caller (DueCadences) treats them as "always due" in any scheduled run, since the scheduled job runs at most once per day and sub-daily cadences effectively fire on every run.

For `every:<duration>` cadences the returned interval is the parsed duration verbatim (no slack — the user picked the number). An unparseable `every:` value returns 0, which the planner treats as "always due" so a misconfigured cadence is loud rather than silently disabling the policy.

func DueCadences added in v0.21.0

func DueCadences(lastSuccess map[string]time.Time, now time.Time) []string

DueCadences returns the set of NAMED cadences that should fire in a scheduled run given the last-success-by-cadence map and the current time. This function is retained for the orchestrator's legacy --cadences filter resolution. The per-policy due decision (which is now authoritative) is in IsDue above.

A cadence is due when (now - last_success) >= CadenceInterval. A cadence absent from the map or with a zero timestamp is treated as never-run and therefore due. continuous and hourly are always due. `every:<duration>` cadences are NOT considered here — they have no canonical bucket; each policy with an every-cadence drives its own due decision via IsDue.

The returned slice is in the canonical scheduledCadences order so callers iterating it observe deterministic behavior across runs.

func DueReason added in v0.21.0

func DueReason(cadence string, state *core.PolicyState, now time.Time) string

DueReason returns a short human-readable reason string for why a policy is (or isn't) due at `now` under the given cadence and state. Powers the `sigcomply why` command and the run-level "skipped because…" diagnostics. Reasons are deterministic given (cadence, state, now) and free of resource identifiers.

func IsDue added in v0.21.0

func IsDue(cadence string, state *core.PolicyState, now time.Time) bool

IsDue reports whether a policy with the given cadence and prior state is due at `now`. The decision composes three layers:

  1. First run (zero LastRunAt) → due.
  2. on_fail_retry: prior status was not pass → due regardless of cadence.
  3. Cadence elapsed: now - LastPassAt >= CadenceInterval(cadence) → due.

Content-hash invalidation (LastPolicyHash mismatch with current hash) is handled by the planner outside this function so the same logic also drives `sigcomply why`-style introspection without needing the current hash.

func IsValidCadence added in v0.21.0

func IsValidCadence(s string) bool

IsValidCadence reports whether s is a recognized cadence — either a named cadence (continuous, hourly, …, annual) or a parseable `every:<duration>` value. Used by the spec validator and the planner's lint warnings.

func NextDueAt added in v0.21.0

func NextDueAt(cadence string, lastPass time.Time) time.Time

NextDueAt computes the wall-clock time after which a policy with the given cadence and last-pass timestamp is due to be re- evaluated. Returns the zero time for "continuous" / "hourly" and for never-run policies — both translate to "always due" in the planner.

For named cadences NextDueAt = lastPass + CadenceInterval (with the embedded cron-drift slack). For `every:<duration>` cadences NextDueAt = lastPass + duration exactly.

func ScheduledCadences added in v0.21.0

func ScheduledCadences() []string

ScheduledCadences returns a copy of the canonical named-cadence list, for callers that need to enumerate every scheduled-mode cadence (e.g. to seed an empty state file or to validate config). Excludes `every:<duration>` forms by design.

func SplitCommaList

func SplitCommaList(s string) []string

SplitCommaList is a small helper for the orchestrator: turn a comma-separated CLI flag value into a trimmed list.

func ValidateCadence added in v0.21.0

func ValidateCadence(s string) error

ValidateCadence returns nil iff s is recognized. For invalid input the error message lists the valid forms.

Types

type Binding

type Binding struct {
	SourceID      string
	AcceptedTypes []string
	CatalogID     string // non-empty only for manual sources (e.g. manual.pdf:access_review_quarterly)
	SlotParams    map[string]any
}

Binding is one resolved (source instance, optional slot params) pair. Multiple bindings on one slot mean "union the records from all of them" — the evaluator merges them per slot.

AcceptedTypes is the intersection of the slot's Accepts list and the source's Emits() list, computed at plan time. The collector passes it through SlotRequest.AcceptedTypes so the plugin knows which of its emitted types the slot will accept. A binding always has at least one AcceptedTypes entry (the planner rejects a binding whose intersection is empty).

type CoverageGap added in v0.30.0

type CoverageGap struct {
	Slot        string   // the unbound required slot
	Accepts     []string // evidence types the slot accepts
	Source      string   // configured source ID emitting a sibling version
	SourceEmits []string // the sibling (family-matching but unaccepted) types it emits
}

CoverageGap flags a required slot that resolved to zero bindings even though a configured source emits a *sibling version* of a type the slot accepts — for example, the slot accepts directory_user.v2 but a configured okta source emits directory_user (v1). The slot cannot bind such a source, so the policy is silently skipped at evaluation (requiredSlotsPopulated → StatusSkip) and disappears from the compliance-score denominator, hiding the gap. The planner records the near-miss so the orchestrator can surface it as an explicit warning.

This is deliberately NOT a hard error: a version may genuinely be unable to answer a policy (the admin-MFA check needs v2-only fields that v1 cannot supply), so the legitimate fix is sometimes "leave it skipped", sometimes "extend accepts:", and sometimes "wire a v2-capable source" — the operator decides.

type Exception

type Exception struct {
	State           core.PolicyStatus // StatusWaived or StatusNA
	Reason          string
	ResourceID      string
	ResourcePattern string
	ApprovedBy      string
	ApprovedAt      string
	ExpiresAt       string
}

Exception is the resolved waiver / N/A declaration applied to a policy. The evaluator uses State to short-circuit (na) or re-classify failures (waived).

type Filter

type Filter struct {
	Policies []string
	Controls []string
	Cadence  string
	OnPush   bool
	Cadences []string
}

Filter narrows the set of policies the plan covers. The five fields are mutually exclusive — passing more than one yields an error. Filter{} means "every policy in the framework."

Cadences is the canonical "fire when these cadences are due" filter — set-intersection against core.PolicyCadences(policy). It is the shape the orchestrator's PR and Scheduled modes use. Cadence and OnPush remain as legacy single-axis shortcuts; they translate to single-element Cadences sets internally.

When any field is set, all matching policies receive ShouldEvaluate=true regardless of cadence state. The filter is the operator's explicit override — "I want these to run now" — and trumps the planner's cadence-elapsed gate.

func (*Filter) IsExplicit added in v0.21.0

func (f *Filter) IsExplicit() bool

IsExplicit returns true when at least one filter axis is set. An explicit filter forces ShouldEvaluate=true for every matching policy — the user said "run these now."

type Input

type Input struct {
	Config        *spec.ProjectConfig
	Registries    *registry.Set
	CommitTime    time.Time
	Now           time.Time
	Filter        Filter
	PolicyStates  map[string]*core.PolicyState
	SchemaDigests map[string]string
}

Input collects everything the planner needs to compute a Plan. CommitTime is the basis for period derivation; pass commit time when time_basis == "commit" (the default) and wall-clock when the project opts into wall_clock.

PolicyStates is keyed by policy_id and provides the per-policy scheduling state read from the vault. A nil map disables cadence gating: every policy in the framework will have ShouldEvaluate set to true. A non-nil map enables gating: each policy's ShouldEvaluate decision composes its cadence, prior status, content-hash, and the operator's filter.

SchemaDigests maps evidence-type IDs to their JSON Schema digests at plan time. Used by PolicyContentHash so a schema bump invalidates prior evaluations of every policy referencing the bumped schema. Pass nil to disable schema-hash discrimination (the policy-spec content still contributes to the hash).

type Period

type Period struct {
	ID        string
	PriorID   string
	Start     time.Time
	End       time.Time
	TimeBasis string // "commit" or "wall_clock"
}

Period is the audit window this run belongs to. Computed from (commit_time, fiscal_calendar) — see docs/architecture/01- conceptual-model.md §Period.

PriorID is the ID of the immediately preceding period under the same calendar (e.g. 2026-Q2 → 2026-Q1, FY2026 → FY2025). Empty when no prior period exists in the configured custom calendar. Plugins use it to compare against the prior period's evidence — currently the manual.pdf plugin reads it to detect copy-paste of last period's PDF.

func DerivePeriod

func DerivePeriod(cfg *spec.PeriodConfig, commit time.Time) (Period, error)

DerivePeriod computes the audit period for a commit timestamp under the project's fiscal_calendar configuration. The function is pure: the only inputs are the configured calendar and the commit time; the only output is a stamped Period. See docs/architecture/01- conceptual-model.md §Period for the model.

type PlannedPolicy

type PlannedPolicy struct {
	Spec       core.Policy
	Cadence    string
	Parameters map[string]any
	Bindings   map[string][]Binding
	Exception  *Exception // nil unless one applies

	ShouldEvaluate bool
	SkipReason     string // empty when ShouldEvaluate is true

	// EvidenceModeOverridden is true when the project config's
	// policy_overrides section changed this policy's evidence_mode from
	// what the framework spec declared. Surfaced in result.json so
	// auditors can see which policies are running in an overridden mode.
	EvidenceModeOverridden bool

	// CoverageGaps lists version-skew near-misses: required slots that
	// resolved to zero bindings while a configured source emits a
	// different *version* of a type the slot accepts. Such policies are
	// silently skipped at evaluation and drop out of the compliance
	// score; the orchestrator warns the operator. Empty for the common
	// case. See CoverageGap.
	CoverageGaps []CoverageGap

	PriorState  *core.PolicyState
	ContentHash string
}

PlannedPolicy is one policy with everything needed to execute it already resolved. Parameters and exceptions are flattened to their effective values so the evaluator never goes back to the config.

ShouldEvaluate is the per-policy gate decided by the planner from the policy's cadence, its on_fail_retry state, the content-hash of the current policy bundle vs the prior run, and the operator's filter overrides. When false, the orchestrator emits a carry- forward result.json that references PriorState.LastEnvelopeRef.

PriorState is the previously-persisted state for this policy, or nil for never-evaluated policies. The orchestrator uses it for carry-forward emission and for the run-summary's freshness view.

ContentHash is the SHA-256 of the canonicalized policy spec + its referenced evidence-type schemas at plan time. Compared against PriorState.LastPolicyHash to detect bundle updates that invalidate prior evaluations.

type RunPlan

type RunPlan struct {
	Framework string
	Period    Period
	Policies  []PlannedPolicy
}

RunPlan is the fully-resolved output of L3. The collector and evaluator iterate it; no further config lookups are needed at run time.

PlannedPolicy entries with ShouldEvaluate=false are present in the plan so the orchestrator can emit a carry-forward result for each, but the collector and evaluator skip them.

func Plan

func Plan(in *Input) (*RunPlan, error)

Plan computes the run plan. Errors surface back to the orchestrator as exit-code-3 conditions. No I/O happens here.

Jump to

Keyboard shortcuts

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