ADR-0008: Change classification, ruleset routing, and rule scope¶
| Status | Accepted (P2-E5) |
| Date | 2026-07-21 |
| Deciders | Konrad Heimel |
| Context links | ADR-0003 change model · ADR-0007 effects |
Context¶
One repo holds many kinds of entries; one MR may touch several. "This part of the diff is a
Kafka-topic change" must route those changes into the topic ruleset; a tfvars edit under
prod/ must route into a stricter binding than the same edit under dev/. Separately, some
rules must see more than the diff: a naming-convention rule should be able to comment on a
touched entry whose pre-existing state violates convention, which requires the full branch
state, not just the changed fields.
Decision (proposed)¶
1. Classification stage¶
After the differ (ADR-0003), a classifier assigns each ChangeSet entry one or more
change classes via declarative matchers (path globs + content predicates, e.g.
file: topics/** or has(new.partitions)), and detects the environment (path convention
or repo config). Classes and environments are just labels — repos define their own.
Unclassified changes get the implicit class unclassified (which no vouch rule should match
→ fail-safe REVIEW per ADR-0007).
2. Ruleset routing¶
A RulesetBinding document maps (change class, environment) → policy packs + risk
threshold. One MR touching topics and tfvars evaluates both packs, each over its slice of
the ChangeSet; aggregation (ADR-0007) runs over the union of findings with the strictest
matching threshold.
3. Rule scope¶
Each rule declares scope:
change(default) — predicate sees the matched ChangeSet entries (old/new values).branch— predicate additionally sees the full repo state at the head SHA (parsed value trees of the checked-out branch). For: conventions on touched-but-not-changed fields, cross-entry uniqueness, referential checks inside the repo.
Determinism holds: head SHA is pinned input; branch scope is still a pure function.
4. Local checkout is mandatory¶
Evaluation always runs against a local checkout of the MR source branch (merged-result checkout where the forge supports it). CI has this for free; webhook mode (ADR-0009) clones per event. No API-only file fetching — partial views breed nondeterminism and n+1 API calls.
Consequences¶
- The classifier is the routing seam: packs stay small and per-domain (topic pack, catalog pack, tfvars pack) and can be shared/versioned independently — the marketplace unit.
branch-scoped rules are costlier (parse the tree); the engine parses lazily per class/glob.- Cross-repo state stays out of scope: that is what fact providers are for (ADR-0004).
Counterpoints considered¶
- "Put match globs on every rule instead of a classifier stage." — Works at small scale, but environment × class × pack routing then lives half-duplicated inside every rule; bindings centralize it and make the risk-threshold table explicit and auditable.
Amendment (2026-07-21, second review P2-10): fail-safe by construction¶
Convention becomes lint hard-errors: (a) a vouch rule must be scoped to at least one
explicit class or non-catch-all path — match: {changes: [{path: "**"}]} with effect
vouch fails assent lint; (b) the unclassified and assent-policy classes are
engine-reserved — a vouch rule matching them is rejected at load, not merely discouraged;
(c) environment matchers declare explicit priority (or must be provably non-overlapping) —
silent order-dependence of "last match wins" is removed, and reordering the list cannot
silently re-route prod to dev thresholds.