Skip to content

fix(aggregate): escalate unmatched edits to REVIEW instead of APPROVE - #151

Merged
konih merged 3 commits into
mainfrom
fm/assent-unmatched-edit-fix
Sep 26, 2026
Merged

konih merged 3 commits into
mainfrom
fm/assent-unmatched-edit-fix

Conversation

@konih

@konih konih commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Intent

An unmatched edit currently fails open to APPROVE. A change that no enforce-phase rule match selects, under a binding that requires an obligation some rule proves, decides APPROVE with zero findings. The cause: cover() marks a required obligation covered as soon as any enforce-phase rule names it in prove.obligation, before it computes matchChanges, so a rule whose match selects none of the governed changes still marks the obligation covered, contributes no finding, and the decision falls through to APPROVE. Reproduced: a policy whose only rule matches valueChanges.pointers ["/partitions"] and proves "non-destructive", under require ["non-destructive"], APPROVEs a governed change at /replicas with an empty finding set. GUIDELINES section 1 (every change must be positively vouched; an empty, broken, or non-matching policy set never auto-merges) and D-142 intend REVIEW. Add an unmatched-edit escalation to REVIEW, mirroring the D-064 unmatched-delete guard: additive, fail-safe, only ever raising toward REVIEW, and a no-op on every change a rule actually governs. Evidence and the exact reproduction: data/assent-adv-dsk/report.md, finding C-1.

What Changed

  • Add an additive, fail-safe aggregate.unmatchedEdit escalation in cover(): a value-level change that no enforce-effective prove rule selects, under a binding that requires an obligation some rule covers, now raises the decision toward REVIEW and emits a change.unmatchedEdit finding, mirroring the D-063/D-064 unmatched-delete guard.
  • Add editGoverned and requiredObligationCovered helpers so only enforce-phase rules govern a change (observe-phase findings stay decision-excluded) and whole-file lifecycle events remain in the fileEvents domain; the guard only ever raises the decision via worse() and is a no-op on fully governed changesets.
  • Add unmatched_edit_test.go regression coverage and update the existing value/paths tests for the new escalation, correct the promotion-gates baseline with a retention-permissive rule that genuinely governs /retentionMs, disable tag signing in the changelog gate sandboxes, and record decision D-185.

Risk Assessment

✅ Low: The change is a small, additive, fail-safe REVIEW escalation that mirrors an existing guard, is a no-op on governed changes, and is covered by a reproduction test; no reachable wrong-result path was found.

Testing

Drove the change end-to-end against the real assent binary built from the worktree. Using the shipped assent test adopter harness on isolated policy repos, the exact C-1 reproduction (a rule matching only /*/partitions that proves non-destructive, under require: [non-destructive], with a governed /replicas edit) now reports REVIEW with an aggregate.unmatchedEdit finding, while a control repo holding the pre-fix APPROVE golden fails and shows the actual REVIEW plus the escalation finding. Governed edits stay APPROVE (clean-true) or take the rule's own BLOCK (clean-false); an unmatched edit beside a governed BLOCK does not relax it; the gate/scope/observe boundaries behave as specified (no require and uncovered require do not emit the new finding, whole-file delete keeps the D-064 guard, whole-file add stays APPROVE). assent compare --suite examples/comparison/promotion-gates runs green and a stripped-baseline control fails closed, confirming the corpus correction is required. All shipped example packs still pass. One scenario — the separately-bundled hack/release/changelog_gate_test.sh tag-signing sandbox fix — could not be driven: this gate worktree is a tagless checkout, so the script fails its §1 release-tag precondition before reaching the sandbox, and it would need a full clone carrying the v0.1.0 release tags plus network to install git-cliff. No product defects found.

  • Live validation: ✅ go - 10 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An unmatched value-level edit under a required, rule-proven obligation decides REVIEW with an aggregate.unmatchedEdit finding (never APPROVE with zero findings) ✅ pass live assent test on /tmp/assent-repro: PASS topics/unmatched-edit (REVIEW); control repo with a pre-fix APPROVE golden FAILs showing actual REVIEW + rule="aggregate.unmatchedEdit" effect="require-review" (…
A governed edit whose rule proves clean-true stays APPROVE with no escalation ✅ pass live assent test /tmp/assent-repro: PASS topics/governed-true (APPROVE)
A governed edit whose rule proves clean-false earns the rule's own BLOCK, not the escalation ✅ pass live assent test /tmp/assent-repro: PASS topics/governed-false (BLOCK) with finding partitions-must-not-shrink
Adversarial additive check: an unmatched edit beside a governed BLOCK does not relax the BLOCK ✅ pass live assent test /tmp/assent-repro: PASS topics/block-preserved (BLOCK)
Adversarial gate: a binding with no require does not escalate an unmatched edit (documented vacuous reading preserved) ✅ pass live assent test /tmp/assent-repro-norequire: PASS topics/unmatched-edit (APPROVE), no aggregate.unmatchedEdit
Adversarial gate: a require with no proving rule REVIEWs via the uncovered guard, with aggregate.unmatchedEdit asserted absent ✅ pass live assent test /tmp/assent-repro-uncovered: PASS topics/unmatched-edit (REVIEW, aggregate.uncovered, absent aggregate.unmatchedEdit)
Adversarial observe boundary: an observe-phase rule selecting the change does not suppress the escalation ✅ pass live assent test /tmp/assent-repro-observe: PASS topics/unmatched-edit (REVIEW)
Scope: an ungoverned whole-file delete keeps the D-064 aggregate.unmatchedDelete guard and a whole-file add stays APPROVE ✅ pass live assent test /tmp/assent-repro inline cases: PASS topics/whole-file-delete (REVIEW, aggregate.unmatchedDelete) and PASS topics/whole-file-add (APPROVE)
Corpus correction: the promotion-gates comparison suite is green, and removing the new baseline rule fails closed ✅ pass live assent compare --suite examples/comparison/promotion-gates exit 0 with all gates PASS; stripped-baseline /tmp copy exit 6 (compare-promotion-gates.txt)
No-op regression: the shipped example policy packs still evaluate green through the real harness ✅ pass live assent test examples/packs/topic-registry, service-catalog, infra-vars all exit 0
The bundled changelog gate sandbox change disables tag signing so the gate runs under a global tag.gpgsign=true ⏸️ untested no This gate worktree is a tagless checkout, so the script exits at its §1 release-tag precondition before reaching the sandbox tag-signing path it changes; it also needs network to install the pinned gi…
Evidence: assent test CLI scenarios (unmatched-edit REVIEW, governed APPROVE/BLOCK, control APPROVE->REVIEW, observe/no-require/uncovered gates, whole-file scope)
\### assent test against isolated reproduction repos (binary: assent 0.0.0-dev)

## Repo A (/tmp/assent-repro): unmatched edit -> REVIEW; governed true -> APPROVE; governed false -> BLOCK; block preserved; whole-file delete/add scope
PASS topics/block-preserved (BLOCK)
PASS topics/governed-false (BLOCK)
PASS topics/governed-true (APPROVE)
PASS topics/unmatched-edit (REVIEW)
PASS topics/whole-file-delete (REVIEW)
PASS topics/whole-file-add (APPROVE)
exit=0

## Control (/tmp/assent-repro-control): the pre-fix APPROVE golden now FAILS showing actual REVIEW + aggregate.unmatchedEdit
PASS topics/governed-false (BLOCK)
PASS topics/governed-true (APPROVE)
FAIL topics/unmatched-edit
  decision: expected APPROVE, got REVIEW
  findings:
    unexpected: rule="aggregate.unmatchedEdit" effect="require-review"
  actual (ready to copy into expect.yaml):
    decision: REVIEW
    findings:
        - rule: aggregate.unmatchedEdit
          effect: require-review
    score:
        total: 0
        threshold: 5
PASS topics/whole-file-delete (REVIEW)
PASS topics/whole-file-add (APPROVE)
exit=1

## Repo B (/tmp/assent-repro-observe): observe-phase rule does NOT govern -> REVIEW
PASS topics/unmatched-edit (REVIEW)
PASS topics/whole-file-delete (REVIEW)
PASS topics/whole-file-add (APPROVE)
exit=0

## Repo C (/tmp/assent-repro-norequire): no require -> gate silent, unmatched edit APPROVEs
PASS topics/governed-true (APPROVE)
PASS topics/unmatched-edit (APPROVE)
PASS topics/whole-file-delete (REVIEW)
PASS topics/whole-file-add (APPROVE)
exit=0

## Repo D (/tmp/assent-repro-uncovered): uncovered require -> uncovered guard only, absent aggregate.unmatchedEdit
PASS topics/unmatched-edit (REVIEW)
exit=0
Evidence: assent compare --suite promotion-gates (green) and the stripped-baseline fail-closed control
\### Scenario 7: assent compare --suite examples/comparison/promotion-gates (corpus correction, RVW-S02-04)

case challenge-intervention-added: baseline=prod-strict@6 candidate=prod-strict@7 delta=stricter-intervention-added
case named-consumer-compat-widen-accepted: baseline=prod-strict@6 candidate=prod-strict@7 delta=newly-auto-mergeable
case partition-grow-agree: baseline=prod-strict@6 candidate=prod-strict@7 delta=(no delta)
case partition-shrink-widen-accepted: baseline=prod-strict@6 candidate=prod-strict@7 delta=newly-auto-mergeable
case score-threshold-change-accepted: baseline=prod-strict@6 candidate=prod-strict@7 delta=score-threshold-change
gate zero-missed-destructive=PASS
gate zero-missed-authorization-ownership=PASS
gate no-unexpected-obligation-removal=PASS
gate bounded-auto-merge-widening=PASS
gate explicitly-accepted-deltas=PASS
exit=0

\### Control: the SAME suite with the RVW-S02 baseline correction REMOVED
# (retention-permissive rule stripped from a /tmp copy of baseline.yaml).
# The fixed engine now REVIEWs the previously-unmatched /retentionMs edit, so the
# case no longer classifies -> fail-closed exit 6. This proves the corpus correction
# is load-bearing: the case really did depend on the unmatched-edit fail-open.
assent compare: compare: case "challenge-intervention-added": compare: decision delta matches none of the classified kinds (fail-closed): identical decision REVIEW but finding identities differ

\### Control: the SAME suite with the RVW-S02 baseline correction REMOVED
# (retention-permissive rule stripped from a /tmp copy of baseline.yaml).
# The fixed engine now REVIEWs the previously-unmatched /retentionMs edit, so the
# case no longer classifies -> fail-closed exit 6. This proves the corpus correction
# is load-bearing: the case really did depend on the unmatched-edit fail-open.
assent compare: compare: case "challenge-intervention-added": compare: decision delta matches none of the classified kinds (fail-closed): identical decision REVIEW but finding identities differ
exit=6

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 10 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An unmatched value-level edit under a required, rule-proven obligation decides REVIEW with an aggregate.unmatchedEdit finding (never APPROVE with zero findings) ✅ pass live assent test on /tmp/assent-repro: PASS topics/unmatched-edit (REVIEW); control repo with a pre-fix APPROVE golden FAILs showing actual REVIEW + rule="aggregate.unmatchedEdit" effect="require-review" (…
A governed edit whose rule proves clean-true stays APPROVE with no escalation ✅ pass live assent test /tmp/assent-repro: PASS topics/governed-true (APPROVE)
A governed edit whose rule proves clean-false earns the rule's own BLOCK, not the escalation ✅ pass live assent test /tmp/assent-repro: PASS topics/governed-false (BLOCK) with finding partitions-must-not-shrink
Adversarial additive check: an unmatched edit beside a governed BLOCK does not relax the BLOCK ✅ pass live assent test /tmp/assent-repro: PASS topics/block-preserved (BLOCK)
Adversarial gate: a binding with no require does not escalate an unmatched edit (documented vacuous reading preserved) ✅ pass live assent test /tmp/assent-repro-norequire: PASS topics/unmatched-edit (APPROVE), no aggregate.unmatchedEdit
Adversarial gate: a require with no proving rule REVIEWs via the uncovered guard, with aggregate.unmatchedEdit asserted absent ✅ pass live assent test /tmp/assent-repro-uncovered: PASS topics/unmatched-edit (REVIEW, aggregate.uncovered, absent aggregate.unmatchedEdit)
Adversarial observe boundary: an observe-phase rule selecting the change does not suppress the escalation ✅ pass live assent test /tmp/assent-repro-observe: PASS topics/unmatched-edit (REVIEW)
Scope: an ungoverned whole-file delete keeps the D-064 aggregate.unmatchedDelete guard and a whole-file add stays APPROVE ✅ pass live assent test /tmp/assent-repro inline cases: PASS topics/whole-file-delete (REVIEW, aggregate.unmatchedDelete) and PASS topics/whole-file-add (APPROVE)
Corpus correction: the promotion-gates comparison suite is green, and removing the new baseline rule fails closed ✅ pass live assent compare --suite examples/comparison/promotion-gates exit 0 with all gates PASS; stripped-baseline /tmp copy exit 6 (compare-promotion-gates.txt)
No-op regression: the shipped example policy packs still evaluate green through the real harness ✅ pass live assent test examples/packs/topic-registry, service-catalog, infra-vars all exit 0
The bundled changelog gate sandbox change disables tag signing so the gate runs under a global tag.gpgsign=true ⏸️ untested no This gate worktree is a tagless checkout, so the script exits at its §1 release-tag precondition before reaching the sandbox tag-signing path it changes; it also needs network to install the pinned gi…
  • Built the product: go build -o /tmp/assent-test-bin ./cmd/assent (reports assent 0.0.0-dev)
  • assent test /tmp/assent-repro — reproduction: unmatched /replicas edit under require: [non-destructive] with a rule matching only /*/partitions reports PASS topics/unmatched-edit (REVIEW); governed clean-true -> APPROVE; governed clean-false -> BLOCK; unmatched edit beside a BLOCK stays BLOCK; whole-file delete -> REVIEW via aggregate.unmatchedDelete; whole-file add -> APPROVE
  • assent test /tmp/assent-repro-control — a pre-fix APPROVE golden for the unmatched edit now FAILS with decision: expected APPROVE, got REVIEW and actual finding rule="aggregate.unmatchedEdit" effect="require-review"
  • assent test /tmp/assent-repro-observe — an observe-phase rule matching the change does NOT suppress the escalation (PASS topics/unmatched-edit (REVIEW))
  • assent test /tmp/assent-repro-norequire — no require keeps the documented vacuous reading (PASS topics/unmatched-edit (APPROVE), no escalation)
  • assent test /tmp/assent-repro-uncovered — a require with no proving rule REVIEWs via aggregate.uncovered with aggregate.unmatchedEdit asserted absent
  • assent compare --suite examples/comparison/promotion-gates — exit 0, all five gates PASS, challenge-intervention-added delta=stricter-intervention-added; a /tmp copy with the baseline correction stripped fails closed (exit 6), proving the corpus correction is load-bearing
  • assent test examples/packs/{topic-registry,service-catalog,infra-vars} — all shipped example packs green (no-op on governed changes)
  • go test ./internal/core/aggregate -run TestUnmatchedEditFailsSafeReview -count=1 and go test ./examples/comparison/... -run TestCompareCorpusRunsGreen -count=1 — targeted regression tests pass
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The gate is meant to be hermetic — its sandbox git helpers already pass
-c commit.gpgsign=false so a global signing config cannot red it — but they
missed tag.gpgsign. With tag.gpgsign=true in a developer global config,
git tag <name> becomes a signed annotated tag that needs a message, so
dgit tag v99.0.0 died with "no tag message?" (and, with an interactive
editor configured, an editor prompt). Add -c tag.gpgsign=false beside
commit.gpgsign=false in every sandbox helper (sgit/ssgit/dgit/ogit/tgit).
The adversarial review (finding C-1, reproduced) showed cover() marks a
required obligation covered as soon as an enforce rule NAMES it in
prove.obligation — before matchChanges runs — so a rule whose match selects
none of the governed changes still marks it covered, contributes no finding,
and the decision falls through to APPROVE with an empty finding set.

A policy whose only rule matches valueChanges.pointers ["/partitions"] and
proves non-destructive, under require [non-destructive], APPROVED a change at
/replicas with zero findings. GUIDELINES Safety-1 and D-142 REQ-DEM-S10-02
both intend REVIEW, and the intended runtime backstop (classify.ValidateRouting)
has no production caller.

Add an additive, fail-safe escalation in cover(), mirroring the D-063/D-064
unmatched-whole-file-DELETE guard: for each value-level change (path != "")
that no enforce-effective prove rule selects, under a binding that requires an
obligation some rule proves, worse(decision, REVIEW) and emit a synthetic
aggregate.unmatchedEdit finding (code change.unmatchedEdit). Scope is
value-level edits only (whole-file deletes are D-064, adds are non-destructive);
observe-phase rules do not govern; one finding per unvouched subject.

The comparison corpus baseline (promotion-gates) gains a permissive
retention-permissive rule: the challenge-intervention-added case had depended
on the fail-open (an ungoverned /retentionMs edit auto-approving), so the
baseline now genuinely governs and permits the edit and the candidate retention
challenge is a real delta again.

Spec: openspec/specs/p5-rvw-review-remediation/spec.md RVW-S02.
@sonarqubecloud

Copy link
Copy Markdown

@konih
konih merged commit 8e3ed5b into main Sep 26, 2026
8 checks passed
@konih
konih deleted the fm/assent-unmatched-edit-fix branch September 26, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant