Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ repository still gets a decision, never by following the link; no release carrie

### Testing
- :white_check_mark: test(cmd): assert the REL-03 error wrap as one contiguous substring
- :test: test(compare): kill the surviving EffectChallenge intervention mutant (TEST-02)
## [0.3.0] - 2026-08-18

### Chores
Expand Down
17 changes: 17 additions & 0 deletions examples/comparison/promotion-gates/candidate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,20 @@ spec:
onFailure:
effect: comment
code: value-bumped
# AUD2-S04 (REQ-AUD2-S04-03): a challenge-effect intervention the baseline
# profile does not carry. It matches only the challenge-intervention-added
# case's /retentionMs pointer, so the other corpus cases are untouched.
- name: retention-ack
phase: enforce
match:
valueChanges:
pointers: ["/retentionMs"]
kinds: [modify]
prove:
obligation: retention-ack
when:
cel: "new >= old"
message: "retention must not shrink without an acknowledged challenge"
onFailure:
effect: challenge
code: retention.shrunk
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"apiVersion": "assent.dev/v1alpha1",
"kind": "ReplayBundle",
"pins": {
"toolVersion": "0.0.0-corpus",
"toolDigest": "sha256:aaaa",
"policySha": "sha256:bbbb",
"sourceSha": "cccc",
"targetSha": "dddd",
"mergeResultDigest": "sha256:eeee",
"factsResolvedAt": {}
},
"evaluationInput": {
"apiVersion": "assent.dev/v1alpha1",
"kind": "EvaluationInput",
"changeSet": {
"changes": [
{
"subject": "topic-registry:orders.events.v1",
"file": "topics/prod/orders.events.v1.yaml",
"path": "/retentionMs",
"kind": "modify",
"old": 604800000,
"new": 86400000
}
]
},
"facts": {},
"mr": {
"author": "alice",
"sourceBranch": "topic/retention",
"targetBranch": "main"
},
"require": ["ownership", "non-destructive"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"apiVersion":"assent.dev/v1alpha1","kind":"ComparisonRecord","baselineProfile":"prod-strict@6","candidateProfile":"prod-strict@7","caseId":"challenge-intervention-added","deltas":[{"kind":"stricter-intervention-added","rule":"retention-ack","subject":"topic-registry:orders.events.v1","obligation":"retention-ack","baseline":{"present":false,"decision":"APPROVE"},"candidate":{"present":true,"decision":"REVIEW","effect":"challenge"}}]}
9 changes: 9 additions & 0 deletions examples/comparison/promotion-gates/suite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ spec:
- caseId: score-threshold-change-accepted
replayBundleDigest: 7a1c59d5c5b3ed12d5fcef2e1651f8e4734e5825688ecdf6e6b25c5920b8d5fa
description: Score-threshold delta allowlisted under explicitly-accepted-deltas gate.
- caseId: challenge-intervention-added
replayBundleDigest: 6e68e17f1d48e682d010de4fac77b7413c8619c6ab6058c4899c8f68b32db815
description: Candidate adds a challenge-effect intervention the baseline APPROVE side lacks (AUD2-S04 / TEST-02).
promotionGates:
- gateId: zero-missed-destructive
failOnKinds: [destructive-or-authorization-intervention-missed]
Expand Down Expand Up @@ -58,3 +61,9 @@ spec:
rule: soft-bump
subject: topic-registry:orders.events.v1
rationale: reviewed score arithmetic tightening
- caseId: challenge-intervention-added
kind: stricter-intervention-added
rule: retention-ack
subject: topic-registry:orders.events.v1
obligation: retention-ack
rationale: reviewed retention-shrink challenge added by the candidate profile
112 changes: 112 additions & 0 deletions internal/compare/classify_intervention_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package compare

import (
"testing"

"github.com/PlatformRelay/assent/internal/core/aggregate"
)

// classify_intervention_test.go defends the intervention-effect predicates against
// the mutant the 2026-08-18 audit demonstrated survives every wired gate (TEST-02):
// deleting `|| e == aggregate.EffectChallenge` from isStricterInterventionEffect
// left `go test ./internal/compare/...` AND `task dogfood-comparison` green.
//
// The cases below are written so the mutant is reached, not short-circuited. classify
// resolves by priority (D-117): missed intervention > uncovered > newly-auto-mergeable
// > score-threshold > stricter-added. A challenge delta lands in the LAST slot, so the
// fixture must keep the four earlier detectors silent or the assertion is decorative:
// - baseline has no findings -> detectMissedIntervention cannot fire
// - no `obligation.uncovered` codes -> isObligationUncovered cannot fire
// - candidate is REVIEW, not APPROVE -> the newly-auto-mergeable branch is skipped
// - intervention keys differ -> isScoreThresholdChange cannot fire
//
// The baseline side must also stay free of challenge findings:
// interventionFindingsByIdentity consults the SAME predicate, so a mutated build
// would drop a baseline challenge out of baseByID and report a spurious `true`,
// masking the kill.

// REQ-AUD2-S04-01 / REQ-AUD2-S04-02: baseline APPROVE with no intervention, candidate
// adds a finding whose effect is `challenge` -> stricter-intervention-added. This is the
// named defence of the EffectChallenge term in isStricterInterventionEffect: deleting
// that term makes classify fail closed (ErrUnclassifiable) and this test red.
func TestClassifyStricterInterventionAddedChallengeEffect(t *testing.T) {
baseline := aggregate.Result{
Decision: aggregate.DecisionApprove,
Findings: nil,
}
candidate := aggregate.Result{
Decision: aggregate.DecisionReview,
Findings: []aggregate.Finding{{
Rule: "retention-ack",
Obligation: "retention-ack",
Effect: aggregate.EffectChallenge,
Subject: "topic-registry:orders.events.v1",
Code: "retention.shrunk",
}},
}

got, err := classify(baseline, candidate)
if err != nil {
t.Fatalf("classify: %v", err)
}
if got != KindStricterInterventionAdded {
t.Fatalf("kind = %q, want %q", got, KindStricterInterventionAdded)
}
}

// REQ-AUD2-S04-01: the predicate itself, asserted directly so the mutant dies even if
// a future classify refactor reroutes the challenge delta to another taxonomy slot.
// isMissedInterventionEffect is asserted alongside it because the two predicates differ
// by exactly one term, and that difference is deliberate (a challenge is NOT a missed
// destructive/authorization intervention) — pinning both stops a "fix" that unifies them.
func TestInterventionEffectPredicates(t *testing.T) {
stricter := map[aggregate.Effect]bool{
aggregate.EffectBlock: true,
aggregate.EffectRequireReview: true,
aggregate.EffectChallenge: true,
aggregate.EffectComment: false,
}
for effect, want := range stricter {
if got := isStricterInterventionEffect(effect); got != want {
t.Errorf("isStricterInterventionEffect(%q) = %v, want %v", effect, got, want)
}
}

missed := map[aggregate.Effect]bool{
aggregate.EffectBlock: true,
aggregate.EffectRequireReview: true,
aggregate.EffectChallenge: false,
aggregate.EffectComment: false,
}
for effect, want := range missed {
if got := isMissedInterventionEffect(effect); got != want {
t.Errorf("isMissedInterventionEffect(%q) = %v, want %v", effect, got, want)
}
}
}

// REQ-AUD2-S04-01: a challenge intervention the baseline ALREADY carries on the same
// delta identity is not "added". This pins the interventionFindingsByIdentity half of
// the predicate's use — the half a lone stricter-added assertion leaves untested, and
// the half through which a mutated build could otherwise answer correctly by accident.
func TestStricterInterventionAddedIgnoresPreexistingChallenge(t *testing.T) {
finding := aggregate.Finding{
Rule: "retention-ack",
Obligation: "retention-ack",
Effect: aggregate.EffectChallenge,
Subject: "topic-registry:orders.events.v1",
Code: "retention.shrunk",
}
baseline := aggregate.Result{
Decision: aggregate.DecisionApprove,
Findings: []aggregate.Finding{finding},
}
candidate := aggregate.Result{
Decision: aggregate.DecisionReview,
Findings: []aggregate.Finding{finding},
}

if detectStricterInterventionAdded(baseline, candidate) {
t.Fatal("detectStricterInterventionAdded = true for a challenge the baseline already carried, want false")
}
}