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 RosterWarnings(cfg *spec.ProjectConfig, set *registry.Set) []string
- func ScheduledCadences() []string
- func SplitCommaList(s string) []string
- func ValidateCadence(s string) error
- func VendorWarnings(cfg *spec.ProjectConfig) []string
- type Binding
- type CoverageGap
- type Exception
- type Filter
- type Input
- type Period
- type PlannedPolicy
- type RosterLink
- 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 the current core.PolicyContentHash — which covers the policy's pass_when body and its referenced schema digests) is handled by decideEvaluation 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 RosterWarnings ¶ added in v0.68.0
func RosterWarnings(cfg *spec.ProjectConfig, set *registry.Set) []string
RosterWarnings returns the non-fatal findings about experimental.roster for the orchestrator to log: unrecognized subkeys (tolerated, per the experimental: hatch) and a roster designated for a framework none of whose policies has a roster slot. Returns nil when the block is absent or malformed (Plan reports the latter as an error).
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.
func VendorWarnings ¶ added in v0.74.0
func VendorWarnings(cfg *spec.ProjectConfig) []string
VendorWarnings reports the non-fatal findings about experimental.vendors: unrecognized subkeys, and configured sources that no register entry claims.
The second is the point. The register is a declaration with nothing to diff it against, so a vendor the operator leaves out is invisible — the one gap the vendor policies cannot close, because they only evaluate what was declared. But a *configured source* is a third party the project demonstrably depends on, named in the operator's own config. Asserting that each one appears in the register is an observed baseline checking a declared one.
It discovers nothing and widens nothing: it reads the sources block the operator already wrote, so it stays entirely inside the chosen estate. Warning, never error — deciding what belongs in the register is the operator's call, and a source may legitimately be out of scope for third-party risk.
Silent when the register is undeclared: a project that never adopted experimental.vendors is not missing anything.
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 — including the full pass_when body — 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. Copied onto the PolicyResult by
// the evaluator and persisted in result.json, so an auditor can see
// which controls are running in an overridden mode — nearly always
// an automated check downgraded to a document upload.
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
// UnboundRequiredSlots names the required slots that resolved to zero
// bindings, sorted. Unlike CoverageGaps (which reports only the
// version-skew near-miss) this is the plain "no configured source
// emits what this slot accepts" case — the policy will be skipped at
// evaluation and will vanish from the compliance-score denominator.
// Consumed by the scope report so an unevaluated control is
// distinguishable from a passing one. Empty for the common case.
UnboundRequiredSlots []string
// Roster carries the project's roster declarations for policies that
// have a roster-role slot; nil otherwise. The evaluator uses it to
// resolve account.* fields (aliases, non-human accounts).
Roster *RosterLink
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 RosterLink ¶ added in v0.68.0
type RosterLink struct {
// Source is the designated roster source ID.
Source string
// Aliases maps source ID → account name → roster email.
Aliases map[string]map[string]string
// NonHuman maps source ID → account names that are not people.
NonHuman map[string][]string
}
RosterLink is the project's experimental.roster block as the evaluator needs it. Account names are lowercased; they match a record's id or its payload.username.
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.