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 ¶
- func CadenceInterval(cadence string) time.Duration
- func DueCadences(lastSuccess map[string]time.Time, now time.Time) []string
- func DueReason(cadence string, state *core.PolicyState, now time.Time) string
- func IsDue(cadence string, state *core.PolicyState, now time.Time) bool
- func IsValidCadence(s string) bool
- func NextDueAt(cadence string, lastPass time.Time) time.Time
- func ScheduledCadences() []string
- func SplitCommaList(s string) []string
- func ValidateCadence(s string) error
- type Binding
- type CoverageGap
- type Exception
- type Filter
- type Input
- type Period
- type PlannedPolicy
- type RunPlan
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CadenceInterval ¶ added in v0.21.0
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
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
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 carry-forward SkipReason 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
IsDue reports whether a policy with the given cadence and prior state is due at `now`. The decision composes three layers:
- First run (zero LastRunAt) → due.
- on_fail_retry: prior status was not pass → due regardless of cadence.
- 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 why-is-this-skipped introspection without needing the current hash.
func IsValidCadence ¶ added in v0.21.0
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
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 ¶
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
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
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 ¶
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.