Documentation
¶
Overview ¶
Package aggregate is the PURE, order-independent obligations aggregator for the P4-E1 walking skeleton (P4-E1-S03, ADR-0017 §2/§6). It evaluates one assert/CEL rule (`prove: {obligation, when}` with `onFailure: {effect, code}`) over the S02 change.ChangeSet and reduces it to a decision APPROVE/REVIEW/BLOCK.
Safety model (the fail-open class this package closes):
- APPROVE is modelled as COVERAGE, never absence-of-failure: the required obligation is APPROVE only if a rule proving exactly that obligation evaluated cleanly to a boolean true. A required obligation with no proving rule, or whose rule errored, is NOT approved — it degrades to REVIEW/BLOCK. This is the ADR-0017 §2 invariant ("every required obligation satisfied to arm"); computing APPROVE as "no rule returned false" would silently approve an unproven obligation, the exact forbidden outcome.
- An opaque ChangeSet (the differ could not decide), an EMPTY change list (schema minItems:1 would be violated downstream), a predicate that fails to COMPILE, a predicate that ERRORS at eval (incl. every numeric-coercion failure — see the CEL binding note below), and a predicate whose result is not a boolean ALL fail SAFE to REVIEW, never APPROVE.
CEL numeric coercion (constraint c). change.Change.Old/New are the differ's CANONICAL, TAG-DISCRIMINATING STRINGS ("12" for int 12, "\"12\"" for the string "12", "016" kept literal). internal/evaldecode INVERTS that render before the engine sees it, so a numeric literal arrives as a json.Number and toCEL binds it as int64/float64 — `new >= old` is a NUMERIC compare and needs no int() in the expression. A non-numeric input to an explicit int()/double() still ERRORS (empirically verified in cel-go: the error surfaces via BOTH the Eval error slot AND a types.Err result value — this package checks both), which the tri-state routes to REVIEW. We deliberately do NOT strconv-coerce Go-side with a 0/false/"" default: that would fail OPEN to APPROVE on a parse failure. A value that is GENUINELY text (a YAML !!str, e.g. `partitions: "12"`) still binds as a string, and a lexical compare over it is wrong in both directions ("6" >= "12" is lexically true, "12" >= "6" lexically false) — so since D-131 evalLeaf's textOrderGuard makes an ordering operator over a text operand an EVALUATION ERROR (-> predicate.error -> REVIEW), never an answer. Ordering quoted numerics deliberately means coercing first: int(new) >= int(old).
Change-ness signal (constraint d). The PRESENCE of an entry in the ChangeSet is the "this field changed" signal. Old==New string-equal can still be a real (tag-only) change the differ emitted; this package NEVER re-derives change-ness from Old vs New. It only binds them for the predicate to read.
Purity (GUIDELINES §5, ADR-0013): this package reads no clock, randomness, environment, or network. cel-go evaluation over fixed inputs is deterministic; the reduction sorts findings by a total key so shuffled input yields a byte-identical decision + findings slice.
Index ¶
- Constants
- Variables
- func CompileCheck(expr string) error
- func CompileMessageTemplate(tmpl string) error
- func EvalScalar(expr string, activation any) (any, error)
- func FactRefFromExpr(expr string) (provider, name string, ok bool)
- func LeafActivation(in EvaluationInput, ch EvalChange, envLabel string) map[string]any
- func ReplaceMessageSlots(tmpl string, replacer func(expr string) (string, error)) (string, error)
- func SensitiveFactAt(activation any, provider, name string) bool
- type ApprovalContext
- type ApprovalEvidence
- type ApprovalPins
- type Approver
- type Binding
- type ChangeSet
- type Decision
- type Effect
- type EvalChange
- type EvaluationInput
- type Fact
- type Finding
- type MR
- type MessageSlot
- type OnFailure
- type ResolvedProfile
- type Result
- func Aggregate(b Binding, cs change.ChangeSet, subjectClass string) (Result, error)
- func Cover(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput) (Result, error)
- func CoverWithApproval(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, ...) (Result, error)
- func CoverWithPhaseCeiling(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, ...) (Result, error)
- func CoverWithProfile(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, ...) (Result, error)
- type Rule
Constants ¶
const ReservedPolicyClass = "assent-policy"
ReservedPolicyClass is the built-in meta-class (ADR-0008/ADR-0015 §1) that an MR editing its own .assent/** policy lands in. A subject in this class DOMINATES to BLOCK independent of any predicate — an MR cannot vouch itself.
Variables ¶
var MessageSlotRE = messageSlotRE
MessageSlotRE is the shared {{ expr }} pattern for compile-check and runtime substitution (E8-S07). One regex so parse/compile/render never drift.
Functions ¶
func CompileCheck ¶
CompileCheck compiles expr against the frozen eleven-field predicate-scope env (newEvalEnv — the single shared source) and returns the compile error, or nil when expr type-checks clean. An out-of-scope top-level identifier surfaces in the returned error as cel-go's `undeclared reference to '<name>'`. The resulting program is budgeted with celCostBudget so a leaf lint accepts is exactly one evalLeaf can build and evaluate; the undeclared-reference detection itself lives entirely in Compile. It is compile-ONLY: it never evaluates, binds no activation, and has no side effects.
func CompileMessageTemplate ¶
CompileMessageTemplate checks every {{ }} slot against newEvalEnv. Unknown top-level identifiers fail at compile time with a located error — never "<no value>" at render (ADR-0016 §2, D-095).
func EvalScalar ¶
EvalScalar compiles and evaluates expr against the frozen predicate-scope env and activation. It returns the native Go value on success. Compile failures, eval errors, cost overruns, and non-scalar error values propagate as errors — never a silent "<no value>".
func FactRefFromExpr ¶
FactRefFromExpr returns provider and fact name when expr selects under facts.*.
func LeafActivation ¶
func LeafActivation(in EvaluationInput, ch EvalChange, envLabel string) map[string]any
LeafActivation builds the CEL activation for one change (E8-S08 render wiring).
func ReplaceMessageSlots ¶
ReplaceMessageSlots substitutes each {{ expr }} slot. replacer receives the trimmed CEL expression; the first error aborts.
func SensitiveFactAt ¶
SensitiveFactAt reports whether activation binds a sensitive fact at provider/name.
Types ¶
type ApprovalContext ¶
type ApprovalContext struct {
// SourceSha is the evaluated source SHA (DecisionRecord pins.sourceSha) the
// decision is being made against. Empty ⇒ every evidence is treated stale
// (fail-closed): a caller that injects evidence but forgets the sha never satisfies.
SourceSha string
// Evidence maps a governed subject entryRef -> its injected ApprovalEvidence.
Evidence map[string]*ApprovalEvidence
}
ApprovalContext is the E2-S07 injection into the decision entry (CoverWithApproval): the evaluated sourceSha and the per-governed-subject pre-fetched evidence. It is a SEPARATE injected input, never a field on the frozen (closed) EvaluationInput (spec judgment call). A nil ApprovalContext or a subject absent from Evidence ⇒ no evidence for that subject ⇒ unsatisfied.
Scoping fence: one ApprovalEvidence per subject satisfies ANY authored require-review firing for that subject in this lane; per-obligation matching via source.rule (a subject carrying two distinct require-review obligations) is NOT modelled here — a documented S07 boundary, not a silent fail-open, because a missing evidence still fails closed.
type ApprovalEvidence ¶
type ApprovalEvidence struct {
// VerifyingCapability is the forge capability that proved this evidence:
// "approval-rules-api" | "codeowners" | "none". "none" is a capability gap.
VerifyingCapability string
// ApprovalsRequired is the forge rule threshold (approval_rules[].approvals_required).
ApprovalsRequired int
// ApprovedBy are the ACTUAL approvers for this rule (approval_state).
ApprovedBy []Approver
// Eligibility is the forge-proven eligible-approver id set (eligibleApproverIds).
// An approver counts toward the threshold only if its id is in this set —
// this is how a bot / non-eligible principal is excluded (ADR-0017 §3
// "forge-proven eligible approval"), the frozen schema carrying no bot flag.
Eligibility []string
// Pins mirrors the DecisionRecord pins the evidence was observed against; only
// SourceSha is load-bearing for staleness in this lane.
Pins ApprovalPins
// Expired is PRE-COMPUTED by the fetch tier (E4), which holds the clock. Like
// facts[].state (S05), expiry state is CARRIED, never recomputed against a
// clock here — internal/core is clock-free (TestCorePurity). The frozen schema
// carries only an expiresAt timestamp; interpreting it against "now" is a
// forge-tier (E4) responsibility, reported as a design flag for S07.
Expired bool
}
ApprovalEvidence is the engine-facing form of the frozen approval-evidence.schema.json — the ONLY contract a require-review obligation may be satisfied against (ADR-0017 §3). Field names mirror the frozen schema (verifyingCapability, approvalsRequired, approvedBy, eligibility, pins) so the Go type is not a fork; LoadApprovalEvidence validates against the frozen schema before decoding into it.
type ApprovalPins ¶
type ApprovalPins struct {
SourceSha string
}
ApprovalPins is the load-bearing slice of the evidence pins for S07: the source SHA the approval was observed against (staleness gate).
type Approver ¶
Approver is one actual approver entry (id/username/isAuthor). The frozen schema forces isAuthor:false on every approvedBy entry (an adapter honesty invariant), so self-approval is caught by identity match to the MR author, with IsAuthor checked as defense-in-depth.
type Binding ¶
type Binding struct {
// Require is the list of obligation names that must each be proven for APPROVE.
Require []string
// Rules are the assert rules; each proves one obligation.
Rules []Rule
// Subject is the governed-subject entryRef this binding evaluates over
// (finding.subject). In the walking skeleton it is the single file subject.
Subject string
}
Binding is the minimal ADR-0017 §2 binding: one require list plus the rules that prove those obligations. AND-only composition (every required obligation must be proven to arm); this slice carries exactly one required obligation.
type ChangeSet ¶
type ChangeSet struct {
Changes []EvalChange `json:"changes"`
}
ChangeSet holds the enumerated changes the decision is evaluated over.
type Decision ¶
type Decision string
Decision is the reduced outcome (ADR-0017 §2). BLOCK dominates REVIEW dominates APPROVE (denies are a union, §2).
const ( // DecisionApprove means every required obligation was proven cleanly true. DecisionApprove Decision = "APPROVE" // DecisionReview is the fail-safe outcome: an undecidable/errored/unproven // obligation, an opaque or empty ChangeSet, or a non-block unsatisfied effect. DecisionReview Decision = "REVIEW" // DecisionBlock is an unsatisfied block effect (or the reserved assent-policy class). DecisionBlock Decision = "BLOCK" )
type Effect ¶
type Effect string
Effect is a rule's onFailure effect (ADR-0017 §2, the DecisionRecord finding schema enum). This thin slice maps an UNSATISFIED effect to a decision: block -> BLOCK; comment/challenge/require-review -> REVIEW. A satisfied obligation contributes no finding and does not lower APPROVE. (require-review is authorization that this provider-less slice cannot forge-prove, so an unsatisfied one stays REVIEW, never APPROVE — ADR-0017 §3.)
const ( // EffectComment is a non-blocking informational effect (ADR-0017 §2). EffectComment Effect = "comment" // EffectChallenge is a resolvable acknowledgement (ADR-0017 §3). EffectChallenge Effect = "challenge" // EffectBlock is a hard block (ADR-0017 §2). EffectBlock Effect = "block" // EffectRequireReview needs forge-proven eligible approval (ADR-0017 §3). EffectRequireReview Effect = "require-review" )
type EvalChange ¶
type EvalChange struct {
Subject string `json:"subject"`
File string `json:"file"`
Path string `json:"path"`
Kind string `json:"kind"`
Old any `json:"old"`
New any `json:"new"`
// Entry is the reconstructed whole-entry object for this change's EntryRef;
// nil = not reconstructed (fall back to New). In-memory enrichment only, NOT
// part of the frozen wire contract — hence json:"-" (LoadEvaluationInput's
// strict decode never reads it; the frozen schema is untouched).
Entry any `json:"-"`
// OldEntry is the pre-image entry object; nil = fall back to Old. In-memory only.
OldEntry any `json:"-"`
}
EvalChange is one governed change: its subject/file/path/kind plus the typed pre/post values (any: json.Number, string, bool, map, slice, or nil).
type EvaluationInput ¶
type EvaluationInput struct {
ChangeSet ChangeSet `json:"changeSet"`
Facts map[string]map[string]Fact `json:"facts"`
MR MR `json:"mr"`
Require []string `json:"require"`
}
EvaluationInput is the Go form of the frozen evaluation-input.schema.json — the engine's decision input (E2-S02). Numeric old/new/fact values are decoded as json.Number so a later numeric compare stays injective (no float64 collapse), mirroring internal/change's numeric discipline.
func LoadEvaluationInput ¶
func LoadEvaluationInput(raw []byte) (*EvaluationInput, error)
LoadEvaluationInput validates raw JSON against the frozen EvaluationInput schema, then decodes it with json.Number semantics so numeric values stay exact. It is pure (no clock/env/network/random).
type Fact ¶
type Fact struct {
State string `json:"state"`
Sensitive bool `json:"sensitive"`
ObservedAt string `json:"observedAt"`
ExpiresAt string `json:"expiresAt,omitempty"`
Value any `json:"value,omitempty"`
Reason string `json:"reason,omitempty"`
}
Fact is a resolved provider fact (typed states; value absent unless resolved).
type Finding ¶
type Finding struct {
Rule string `json:"rule"`
Obligation string `json:"obligation,omitempty"`
Effect Effect `json:"effect"`
Subject string `json:"subject"`
Points int `json:"points"`
Code string `json:"code,omitempty"`
// Message is the failing leaf's expanded per-leaf message (ADR-0013 E2-S03):
// when an all/any/not (or single-leaf) `when` is unsatisfied, the attributed
// leaf's `message` — with {{ old }}/{{ new }}/{{ facts.* }} template expansion
// over the SAME activation model the CEL leaf saw — names WHICH conjunct failed.
// omitempty keeps every pre-S03 finding (bare-string/no-message leaves, incl.
// the D-016 golden) byte-identical, and record.go does not project it into the
// serialized DecisionRecord finding (the frozen schema has no message field).
Message string `json:"message,omitempty"`
}
Finding is one emitted obligation-proving-rule outcome, shaped after the DecisionRecord #/$defs/finding object (S04 serializes the full record; this package produces the finding set and the decision). A finding is emitted for an UNSATISFIED or UNDECIDABLE obligation only; a satisfied obligation is silent.
type MR ¶
type MR struct {
Author string `json:"author"`
SourceBranch string `json:"sourceBranch"`
TargetBranch string `json:"targetBranch"`
Labels []string `json:"labels,omitempty"`
}
MR is the merge-request metadata bound to the `mr` predicate-scope field.
type MessageSlot ¶
type MessageSlot struct {
Expr string
Offset int // byte offset of '{{' in the template
Line int
Column int
}
MessageSlot is one {{ expr }} region in a message template.
func ParseMessageSlots ¶
func ParseMessageSlots(tmpl string) []MessageSlot
ParseMessageSlots extracts CEL slots from a message template.
type OnFailure ¶
OnFailure is a rule's failure declaration (ADR-0017 §2): the effect applied and the stable finding code echoed into the DecisionRecord/PresentationModel.
type ResolvedProfile ¶
type ResolvedProfile struct {
// Name is the resolved profile's metadata.name (empty when none resolved).
Name string
// Writes is the resolved profile's spec.writes — true only for a covering
// writes:true profile; a recorder-only profile is false.
Writes bool
}
ResolvedProfile is the outcome of profile resolution for one binding: the resolved profile's identity and whether it holds forge write authority.
func ResolveProfile ¶
func ResolveProfile(precedence []policy.ProfileRef, profiles []*policy.Profile, env, class string) (ResolvedProfile, bool, error)
ResolveProfile resolves the single covering profile for the (environment, class) binding from the Config.profiles precedence table and the handed profile documents. It returns the resolution, whether any profile covered the binding, and an error only when the config is invalid (single-writer violation or a dangling precedence ref — both fail-closed at load, never a silent pick).
Resolution order (ADR-0018 §2): coverage → SPECIFICITY (narrower scope wins) → config-order tie-break. The covering set arrives IN PRECEDENCE ORDER from policy.CoveringProfiles, so a true specificity tie is broken by keeping the FIRST (earlier-in-table) covering profile — deterministic and driven by the precedence table, not input-slice or map iteration order.
type Result ¶
type Result struct {
Decision Decision `json:"decision"`
Findings []Finding `json:"findings"`
// Observed carries the findings produced by OBSERVE-phase rules (E2-S08,
// ADR-0018 §1). They are evaluated and recorded but STRUCTURALLY EXCLUDED from
// aggregation — they never enter the decision reduction, the points sum, or the
// capability-gap set (Findings is the enforcing bucket that does). Routed here
// at the point of production, not filtered post-hoc. Canonically sorted like
// Findings. omitempty keeps the no-observe Result (the D-016 golden) byte-
// identical, and record.go threads it into DecisionRecord findings.observed
// (was hardcoded []).
Observed []Finding `json:"observed,omitempty"`
// CapabilityGaps records, per governed subject, a forge capability gap
// discovered while satisfying a require-review obligation (E2-S07): an
// injected ApprovalEvidence with verifyingCapability:none. It is the
// aggregate-layer precursor to DecisionRecord pins.capabilityGap (S10 threads
// it there); recorded here so a capability gap stays DISTINCT from a plain
// missing approval (a require-review finding with no gap) — the
// d016_missing_approval invariant. omitempty keeps the no-evidence Result
// (D-016 golden) byte-identical. A gap NEVER satisfies, so the require-review
// finding still stands and the run can never auto-merge.
CapabilityGaps map[string]string `json:"capabilityGaps,omitempty"`
// Profile is the resolved covering profile's identity (E2-S09, ADR-0018 §2),
// stamped by WithProfile. Empty when no profile covers the binding (or none
// were declared — the D-016 case). Surfaced at the engine layer only; E4
// threads it into the DecisionRecord once the frozen schema carries the field.
Profile string `json:"profile,omitempty"`
// WriteAllowed is whether the resolved profile holds forge write authority
// (spec.writes) for this binding (E2-S09). It is the SAFE value false unless a
// single covering writes:true profile resolved — a recorder-only (writes:false)
// profile never sets it, and an uncovered/undeclared binding defaults to false.
// A downstream forge step reads it to know whether this run may arm/merge.
// omitempty keeps the no-profile Result (the D-016 golden) byte-identical.
WriteAllowed bool `json:"writeAllowed,omitempty"`
}
Result is the aggregator output: the reduced decision and the canonically sorted findings that justify it. Findings are sorted by a TOTAL key so a shuffled rule input yields a byte-identical Result (REQ-P4-E1-S03-03).
func Aggregate ¶
Aggregate evaluates the binding's single-obligation rules over the S02 ChangeSet and reduces to a decision, fail-safe throughout.
S07-01 SEAM: subjectClass is the per-subject class signal a later serialized edit (internal/core/classify, another lane) computes. When it equals ReservedPolicyClass the aggregator SHORT-CIRCUITS to BLOCK *before any predicate evaluation* — the reserved-class meta-block dominates even a satisfied assert (ADR-0008 amendment). This lane does NOT build the classifier; it only leaves this dominating hook so S07-01 wires in cleanly. Pass "" when no class is known.
func Cover ¶
func Cover(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput) (Result, error)
Cover computes the multi-obligation × multi-subject decision over the loaded merge policy, binding, and evaluation input, with NO injected approval evidence — every require-review obligation stays unsatisfied (the D-016 golden path). It is the stable 3-arg entry preserved byte-identical for existing callers.
func CoverWithApproval ¶
func CoverWithApproval(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, appr *ApprovalContext) (Result, error)
CoverWithApproval is the E2-S07 evidence-aware decision entry: it additionally takes a separately-injected ApprovalContext (the evaluated sourceSha + per- governed-subject pre-fetched ApprovalEvidence) so an authored require-review obligation can be SATISFIED by valid, eligible, sha-matching, non-expired, non-self/bot approval (ADR-0017 §3). A nil appr is exactly Cover. Evidence is injected as a second input, never a field on the frozen EvaluationInput.
func CoverWithPhaseCeiling ¶
func CoverWithPhaseCeiling(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, appr *ApprovalContext, ceiling policy.Phase) (Result, error)
CoverWithPhaseCeiling is the E2-S08 pack-ceiling decision entry: it additionally takes a pack-level phase CEILING (ADR-0018 §1) that DOWNGRADES every rule's effective phase to min(rule.phase, ceiling) on the off<observe<enforce ordering. The ceiling only ever caps toward off — it is never additive:
- ceiling enforce ⇒ each rule's own phase stands (no cap) — exactly Cover;
- ceiling observe ⇒ every rule caps at observe (an enforce rule inside an observe pack runs as observe → its finding lands in the observed bucket);
- ceiling off ⇒ nothing in the pack evaluates.
The ceiling is threaded as a PARAMETER (default PhaseEnforce = no cap) because the frozen MergePolicy carries no spec.phase — only a Pack does (spec.phase), and Cover works over MergePolicy. A caller that has loaded a Pack passes its spec.phase here; a caller with no pack passes enforce (or uses Cover). An empty ceiling is normalized to enforce (no cap) so a caller slip never caps everything off.
func CoverWithProfile ¶
func CoverWithProfile(pol *policy.MergePolicy, bind *policy.Binding, in *EvaluationInput, appr *ApprovalContext, ceiling policy.Phase, precedence []policy.ProfileRef, profiles []*policy.Profile) (Result, error)
CoverWithProfile is the E2-S09 profile-aware decision entry: it resolves the covering profile for the binding's (environment, class) BEFORE producing the decision, so a single-writer violation or a dangling precedence ref fails the whole run closed (no Result is returned), then evaluates the coverage loop and stamps the resolved write-authority + identity onto the Result. Profile resolution never alters the decision or the finding set — it only surfaces whether this run may write. A caller with no profiles passes an empty precedence table (⇒ no covering profile ⇒ no write authority, the safe default).
func (Result) WithProfile ¶
func (r Result) WithProfile(rp ResolvedProfile, resolved bool) Result
WithProfile stamps a resolution onto the Result: the resolved profile identity and its write authority. Write authority is the SAFE default (false) unless a single covering writes:true profile resolved — so a recorder-only profile surfaces its identity but never sets WriteAllowed, and an unresolved binding leaves both zero (recorder-only / no-write default). A downstream forge step reads WriteAllowed to know whether this run may arm/merge or is recorder-only.
type Rule ¶
type Rule struct {
// Name identifies the rule (finding.rule); part of the canonical sort key.
Name string
// Obligation is the single obligation this rule proves (prove.obligation).
Obligation string
// When is the CEL assert expression; it may reference old, new, and changes
// (see bindActivation). It MUST evaluate to a boolean; a non-bool result
// fails safe to REVIEW.
When string
// OnFailure is applied when When is cleanly false.
OnFailure OnFailure
}
Rule is one assert/CEL rule that proves exactly one obligation (ADR-0017 §2). This is the MINIMAL rule type the walking skeleton needs — multi-obligation composition, points/scoring, and require-review authorization are E2.