ADR-0007: Rule effects and decision aggregation (incl. risk points)¶
| Status | Accepted (partial: vouch/coverage superseded by ADR-0017 §2; challenge authz by §3; P2-E5) |
| Date | 2026-07-21 |
| Deciders | Konrad Heimel |
| Context links | ADR-0002 policy surface · ADR-0008 routing · ADR-0005 forge |
Context¶
"A rule fired" is not one thing. In practice a rule's outcome is one of: an informational comment; a resolvable "are you sure?" thread that must be acknowledged before merge; a hard block (with optional explanation); or a positive signal that a change is safe to auto-approve. Severity levels don't capture this — these are effects with different forge actions and different aggregation semantics. Additionally, a scalar risk score is wanted so that many small oddities can add up to "human, please look" even when no single rule blocks.
Decision (proposed)¶
Effects (per rule, declared in the envelope)¶
| Effect | Meaning | Forge action | Blocks merge? |
|---|---|---|---|
comment |
informational finding | plain MR/PR comment (batched) | no |
challenge |
"are you sure?" | resolvable thread; merge waits for resolution | until resolved |
block |
hard deny | thread/comment (optional custom message) + deny | yes |
vouch |
this change is understood and safe | none (recorded in report) | enables automerge |
score |
contribute risk points (points: N) |
none (recorded) | via threshold |
A rule = match + predicate + one effect (plus optional points, allowed alongside any
effect). Findings carry rule id, effect, paths, message, points.
Aggregation (deterministic, order-independent)¶
- Any
blockfinding → BLOCK. - Else any unresolved
challenge→ REVIEW (threads posted; on the forge the MR merges only after all threads are resolved and re-evaluation passes). - Else coverage check: every entry in the ChangeSet must be matched by ≥1
vouchrule. Unvouched changes → REVIEW with an explicit "uncovered change" finding. Fail-safe by construction: an empty or non-matching policy set never automerges anything. - Else risk check:
sum(points)≤ threshold for the active (environment, change class) binding (ADR-0008) → APPROVE (+ merge); over threshold → REVIEW.
comment findings are emitted in every outcome. The JSON report always contains the full
finding list, per-rule traces, score arithmetic, and the aggregation path taken.
Consequences¶
- Auto-approve is not an effect a single rule grants unilaterally; it is the aggregate of full vouch coverage + no blocks/challenges + score under threshold. This keeps the default-deny posture while letting packs be composed.
- The forge port (ADR-0005) must support: batched comments, resolvable threads, deny, approve
- merge — already in scope;
challengeresolution semantics on GitHub is OQ-7. - Open sub-questions: score scale conventions and whether thresholds may also downgrade
(e.g. force
challenge→blockin prod) — OQ-13.
Counterpoints considered¶
- "Severity levels are simpler." — They conflate presentation with control flow; mapping severities to forge actions per repo would end up re-inventing exactly this effect table, but implicitly.
- "Vouch-coverage is annoying; default-allow with deny rules is less work." — Default-allow automerge on config repos is how outages happen; annoyance is the feature.
Amendment (2026-07-21, adversarial review F6/F7/F10)¶
Tri-state predicates (F6). A predicate evaluates to true / false / error (missing
fact, type mismatch, cost-limit hit, undefined). Error is fail-safe by effect: on a vouch
rule it means no vouch; on a block/challenge/comment rule the effect fires,
with the error surfaced in the finding details. An error can never silently make a rule
"not fire" in the permissive direction.
Cross-pack coverage (F10). When multiple packs are routed (union of findings, strictest
threshold), coverage is: an entry is covered iff at least one routed pack vouches it, and
any block/challenge from any routed pack stands — union of denies, single-vouch
trust. A strict pack that must not be undercut by a broad base-pack vouch declares
coverage: exclusive in its pack.yaml, requiring the vouch to come from itself for the
classes it owns. The multi-pack worked example belongs in the Phase-3 spec.
Score is intra-MR only (F7). Risk points do not accumulate across MRs (stateless, D-007); salami-slicing a large change into many small MRs defeats the threshold by design. Operators must not read the score histogram as a rate limiter; cross-MR accumulation would require serve-mode state and is explicitly out of scope for v1.
Amendment 2 (2026-07-21, second review P1-6/P1-7)¶
Points multiplicity. The predicate runs once per matched change (ADR-0011 amendment);
points accrue per firing, not per rule. vouch + points is therefore the built-in
bulk-change guard: ten vouched partition bumps at points: 1 against a prod threshold of 4
→ REVIEW, by design ("individually safe, collectively worth a look"). Starter packs must
use points sparingly and the docs must state this multiplication explicitly.
Rego vouch polarity. Rego modules are violations-shaped; a violation feeds
comment/challenge/block effects. A rego-backed vouch rule must return the explicit
set of vouched change paths (vouch contains path if { … }); anything not in the set stays
uncovered. No implicit "no violation = vouch".
onFail branch (kills negation pairs). A rule may declare an onFail: block
(effect, message, points) applied to matched changes whose predicate is false —
one predicate, both outcomes, no hand-negated twin rules that drift. The shipped
bounded-change example demonstrated the failure this fixes: vouch on
new >= old && new <= quota left the quota-exceeded case silently uncovered with no
message; with onFail: {effect: challenge, message: "…exceeds quota…"} the contributor gets
told. Predicate error remains its own case (tri-state, amendment 1): errors never take
the onFail branch — they fail safe by effect.