Skip to content

feat(cli): enforce ADR writing order with structural gates#10

Merged
lucifer1004 merged 1 commit into
mainfrom
feat/adr-gate
Apr 15, 2026
Merged

feat(cli): enforce ADR writing order with structural gates#10
lucifer1004 merged 1 commit into
mainfrom
feat/adr-gate

Conversation

@lucifer1004
Copy link
Copy Markdown
Contributor

@lucifer1004 lucifer1004 commented Apr 14, 2026

Summary

Add structural ADR completeness gates so decision and accept follow the repo's alternatives-first ADR writing model.

What Changed

  • block govctl adr set/edit ... decision when alternatives are incomplete
  • block govctl adr accept when the ADR does not yet have a complete alternatives set
  • require ADR completeness to include:
    • at least 2 alternatives
    • at least 1 accepted alternative
    • at least 1 rejected alternative
  • allow govctl adr accept --force for historical backfills where alternatives cannot be reconstructed
  • add shared ADR completeness validation and regression tests
  • record the decision in ADR-0042 and close WI-2026-04-14-001

Behavior

  • writing decision now fails early if the ADR has no alternatives, only one alternative, or no rejected alternative
  • govctl adr accept now enforces the same completeness gate before moving to accepted
  • govctl adr accept --force bypasses the structural gate for backfill scenarios

Governance

  • ADR-0042: accepted (enforce ADR writing order with structural gates)
  • WI-2026-04-14-001: completed
  • RFC-0002 docs updated with the new command/lifecycle expectations

Verification

  • cargo test -q
  • cargo run --quiet -- check
  • cargo clippy --all-targets --all-features -- -D warnings

Notes

  • this is intentionally opinionated: ADRs must show alternatives before conclusion
  • --force exists only to support historical migration/backfill cases

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --force flag to the adr accept command to bypass validation requirements.
  • Enhancements

    • Setting an ADR decision now requires alternatives to be complete: a minimum of 2 alternatives with at least one marked accepted and one marked rejected.
    • Accepting an ADR now enforces completeness validation for all required fields and alternatives completeness; can be bypassed using the --force flag.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Warning

Rate limit exceeded

@lucifer1004 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 19 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 18 minutes and 19 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f971009-355e-4b89-8077-6a83938b9023

📥 Commits

Reviewing files that changed from the base of the PR and between 5a566bf and e9092fe.

⛔ Files ignored due to path filters (15)
  • tests/snapshots/test_adr_gates__accept_blocked_with_only_one_alternative.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_blocked_without_accepted.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_blocked_without_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_force_bypasses_gates.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_succeeds_with_complete_adr.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_via_legacy_dotted_path.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_with_only_one_alternative.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_without_accepted.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_without_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_without_rejected.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_succeeds_with_complete_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_edit__adr_set_decision.snap is excluded by !**/*.snap
  • tests/snapshots/test_edit__path_backward_compat.snap is excluded by !**/*.snap
  • tests/snapshots/test_lifecycle__accept_already_accepted_fails.snap is excluded by !**/*.snap
  • tests/snapshots/test_lifecycle__accept_proposed_adr.snap is excluded by !**/*.snap
📒 Files selected for processing (11)
  • CHANGELOG.md
  • gov/adr/ADR-0042-enforce-adr-writing-order-with-structural-gates.toml
  • gov/work/2026-04-14-implement-adr-writing-order-gates.toml
  • src/cli.rs
  • src/cmd/edit/mod.rs
  • src/cmd/lifecycle.rs
  • src/command_router.rs
  • src/resource_plan.rs
  • tests/test_adr_gates.rs
  • tests/test_edit.rs
  • tests/test_lifecycle.rs
📝 Walkthrough

Walkthrough

This PR implements ADR writing order governance gates that enforce completeness validation at two lifecycle points: when setting an ADR's decision field (requires at least 2 alternatives with both accepted and rejected statuses), and when accepting an ADR. Both gates support a --force flag bypass.

Changes

Cohort / File(s) Summary
Documentation & Planning
CHANGELOG.md, gov/adr/ADR-0042-enforce-adr-writing-order-with-structural-gates.toml, gov/work/2026-04-14-implement-adr-writing-order-gates.toml
Added changelog entry, new ADR-0042 documenting the structural gates policy for enforcing ADR writing order, and work item tracking implementation with completion status and acceptance criteria.
CLI & Command Infrastructure
src/cli.rs, src/command_router.rs, src/resource_plan.rs
Updated AdrCommand::Accept to use explicit struct with force: bool flag; propagated force parameter through LifecycleOp::AcceptAdr enum variant and related pattern-matching in command routing and resource planning.
Validation & Lifecycle Logic
src/cmd/edit/mod.rs, src/cmd/lifecycle.rs
Added new validate_adr_completeness() function that checks for minimum 2 alternatives with at least one accepted and one rejected; integrated validation gate into decision-setting flow and updated accept_adr() signature to accept force parameter, conditionally bypassing completeness validation.
Tests
tests/test_adr_gates.rs, tests/test_edit.rs, tests/test_lifecycle.rs
Added comprehensive integration test module covering decision-gate blocking scenarios (incomplete alternatives, single alternative, missing rejected), accept-gate blocking, force-bypass behavior, and success paths; updated existing edit and lifecycle tests to pre-populate alternatives before decision-setting and accept operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Hopping through decisions, careful and precise,
Alternatives first—no shortcuts, no vice!
Gates guard the order, --force saves the day,
Write-once, decide-once, the right rabbit way!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: enforcing ADR writing order through structural gates, which is the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adr-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 91.52542% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/cmd/lifecycle.rs 90.38% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
tests/test_adr_gates.rs (1)

66-109: Add the symmetric “no accepted alternative” failure case.

You already test “no rejected”; adding a case with no accepted alternative would fully lock the invariant matrix for both adr set ... decision and adr accept.

Also applies to: 178-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_adr_gates.rs` around lines 66 - 109, Add a symmetric test that
verifies setting a decision fails when there are alternatives but none are
accepted: copy the existing test_set_decision_blocked_without_rejected and
create test_set_decision_blocked_without_accepted which creates two
alternatives, marks both as "rejected" via the same tick commands (use -s
rejected for both indices), runs adr set ADR-0001 decision ..., and asserts the
output contains "alternatives incomplete" and snapshot; also add the analogous
symmetric test for the acceptance gate (the test near the other block around the
accept-related tests) to cover the "no accepted alternative" failing case for
adr accept/accept-related commands so both gate invariants are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 14: Update the changelog bullet to precisely state that the --force flag
is attached to the adr accept command rather than both gates; e.g., change "Both
gates bypassable with --force" to wording like "adr accept supports --force to
bypass gates" (mention the specific command symbol adr accept and the flag
--force) so the entry matches the PR behavior.

In `@gov/adr/ADR-0042-enforce-adr-writing-order-with-structural-gates.toml`:
- Around line 41-45: Update ADR-0042 wording so the gate contract matches the
CLI: remove the blanket statement that acceptance requires non-empty `context`,
`decision`, and `consequences` and instead state that acceptance is
alternatives-based (minimum alternative count and evaluated-alternatives rules
are the deciding criteria per the `adr-writer` skill). Also clarify that the
`--force` flag is exposed on the `adr accept` command to bypass the write-time
gate (rather than implicitly bypassing both gates), and avoid restating
implementation details; reference the `adr-writer` ordering and the
minimum-deliberation threshold as guidance.

In `@gov/work/2026-04-14-implement-adr-writing-order-gates.toml`:
- Line 18: The journal entry (content.journal) says "all tests pass" but the
test-status field "Unit tests for both gates" remains "pending", so update the
test-status entry to reflect success (e.g., change "Unit tests for both gates"
from pending to passed/complete) or, alternatively, change content.journal to
match the pending state; locate the fields named content.journal and the
test-status entry "Unit tests for both gates" in the TOML (including the related
entries at the other occurrence on lines 36-38) and make them consistent.
- Line 13: The description currently claims "Both gates support --force", which
is incorrect and conflicts with the PR that only exposes --force on the adr
accept path; update the description text to state that only the Lifecycle gate
(used by the "adr accept" command) supports a --force override for historical
backfills, and explicitly note the Write-time gate blocks setting decision until
alternatives are complete and is not bypassable; make the same correction in the
other occurrences of this wording (the description and the repeat on lines
referencing the Write-time gate and Lifecycle gate).

In `@src/cmd/edit/mod.rs`:
- Around line 437-440: Update the comment above the invariant gate to cite the
specific RFC clause that authorizes this lifecycle/edit check in addition to
ADR-0042: edit the line starting with "// Implements [[ADR-0042]]: block setting
`decision` without complete alternatives" to include the RFC identifier and
clause number (e.g., "RFC-XXXX §Y.Z") and a short phrase linking it to the
normative invariant; keep the code using ArtifactType::Adr, fp.as_simple() ==
Some("decision"), and crate::cmd::lifecycle::validate_adr_completeness(config,
id)? unchanged.
- Around line 438-440: Add a regression test that mirrors
test_set_decision_blocked_without_alternatives but uses the legacy dotted path
"content.decision": call the command flow through plan_mutation_request() (so
path normalization occurs) and attempt `adr set <ID> content.decision` on an ADR
with incomplete alternatives, then assert that
crate::cmd::lifecycle::validate_adr_completeness blocks the operation (i.e., the
command returns the same failure as the non-dotted variant). Ensure the new test
invokes the same setup/fixtures as
test_set_decision_blocked_without_alternatives and checks for the same
error/exit condition to confirm the gate at ArtifactType::Adr + fp.as_simple()
== Some("decision") is enforced for the dotted legacy path.

In `@src/cmd/lifecycle.rs`:
- Around line 297-300: Update the doc comment for the function
validate_adr_completeness (and the related ADR comment at the other nearby
comment block referenced in the review) to append the exact normative RFC clause
identifier(s) that define the enforced invariants (requirement of at least 2
alternatives, and at least 1 accepted and 1 rejected). Do not paraphrase—insert
the official RFC clause IDs/section numbers (e.g., "RFC-XXXX, Section Y.Z" or
the precise clause tokens) into the comment so the code directly cites the
governing normative text; apply the same insertion to the second commented
invariant noted in the review.

---

Nitpick comments:
In `@tests/test_adr_gates.rs`:
- Around line 66-109: Add a symmetric test that verifies setting a decision
fails when there are alternatives but none are accepted: copy the existing
test_set_decision_blocked_without_rejected and create
test_set_decision_blocked_without_accepted which creates two alternatives, marks
both as "rejected" via the same tick commands (use -s rejected for both
indices), runs adr set ADR-0001 decision ..., and asserts the output contains
"alternatives incomplete" and snapshot; also add the analogous symmetric test
for the acceptance gate (the test near the other block around the accept-related
tests) to cover the "no accepted alternative" failing case for adr
accept/accept-related commands so both gate invariants are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a621dbfe-e137-4eaa-8059-f26346c45bc3

📥 Commits

Reviewing files that changed from the base of the PR and between f720277 and 5a566bf.

⛔ Files ignored due to path filters (12)
  • tests/snapshots/test_adr_gates__accept_blocked_with_only_one_alternative.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_blocked_without_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_force_bypasses_gates.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__accept_succeeds_with_complete_adr.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_with_only_one_alternative.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_without_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_blocked_without_rejected.snap is excluded by !**/*.snap
  • tests/snapshots/test_adr_gates__set_decision_succeeds_with_complete_alternatives.snap is excluded by !**/*.snap
  • tests/snapshots/test_edit__adr_set_decision.snap is excluded by !**/*.snap
  • tests/snapshots/test_edit__path_backward_compat.snap is excluded by !**/*.snap
  • tests/snapshots/test_lifecycle__accept_already_accepted_fails.snap is excluded by !**/*.snap
  • tests/snapshots/test_lifecycle__accept_proposed_adr.snap is excluded by !**/*.snap
📒 Files selected for processing (11)
  • CHANGELOG.md
  • gov/adr/ADR-0042-enforce-adr-writing-order-with-structural-gates.toml
  • gov/work/2026-04-14-implement-adr-writing-order-gates.toml
  • src/cli.rs
  • src/cmd/edit/mod.rs
  • src/cmd/lifecycle.rs
  • src/command_router.rs
  • src/resource_plan.rs
  • tests/test_adr_gates.rs
  • tests/test_edit.rs
  • tests/test_lifecycle.rs

Comment thread CHANGELOG.md Outdated
Comment thread gov/adr/ADR-0042-enforce-adr-writing-order-with-structural-gates.toml Outdated
Comment thread gov/work/2026-04-14-implement-adr-writing-order-gates.toml Outdated
Comment thread gov/work/2026-04-14-implement-adr-writing-order-gates.toml
Comment thread src/cmd/edit/mod.rs
Comment on lines +437 to +440
// Implements [[ADR-0042]]: block setting `decision` without complete alternatives
if artifact == ArtifactType::Adr && fp.as_simple() == Some("decision") {
crate::cmd::lifecycle::validate_adr_completeness(config, id)?;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add RFC clause citation for this invariant gate.

This enforces a lifecycle/edit invariant in Rust code, but the comment cites only ADR-0042. Please add the corresponding RFC clause reference used as normative authority for this check.

As per coding guidelines: src/**/*.rs: Cite RFC clauses when implementing invariants in code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/edit/mod.rs` around lines 437 - 440, Update the comment above the
invariant gate to cite the specific RFC clause that authorizes this
lifecycle/edit check in addition to ADR-0042: edit the line starting with "//
Implements [[ADR-0042]]: block setting `decision` without complete alternatives"
to include the RFC identifier and clause number (e.g., "RFC-XXXX §Y.Z") and a
short phrase linking it to the normative invariant; keep the code using
ArtifactType::Adr, fp.as_simple() == Some("decision"), and
crate::cmd::lifecycle::validate_adr_completeness(config, id)? unchanged.

Comment thread src/cmd/edit/mod.rs
Comment thread src/cmd/lifecycle.rs
Comment on lines +297 to +300
/// Validate ADR alternatives completeness per [[ADR-0042]].
///
/// Requires at least 2 alternatives, with at least 1 accepted and 1 rejected.
pub fn validate_adr_completeness(config: &Config, adr_id: &str) -> anyhow::Result<()> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add RFC clause references for these newly enforced invariants.

These invariant comments currently cite [[ADR-0042]] only. Please also cite the exact normative RFC clause ID(s) that define this behavior so the implementation references the governing RFC directly in code.

As per coding guidelines src/**/*.rs: "Cite RFC clauses when implementing invariants in code".

Also applies to: 349-350

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/lifecycle.rs` around lines 297 - 300, Update the doc comment for the
function validate_adr_completeness (and the related ADR comment at the other
nearby comment block referenced in the review) to append the exact normative RFC
clause identifier(s) that define the enforced invariants (requirement of at
least 2 alternatives, and at least 1 accepted and 1 rejected). Do not
paraphrase—insert the official RFC clause IDs/section numbers (e.g., "RFC-XXXX,
Section Y.Z" or the precise clause tokens) into the comment so the code directly
cites the governing normative text; apply the same insertion to the second
commented invariant noted in the review.

Add write-time gate blocking `adr set decision` without complete
alternatives (>= 2, with 1 accepted and 1 rejected), and lifecycle
gate blocking `adr accept` for incomplete ADRs. Both gates use
shared `validate_adr_completeness()` helper. Accept gate supports
`--force` for historical backfills.

ADR-0042: accepted.
WI-2026-04-14-001: done.
@lucifer1004 lucifer1004 merged commit bae5691 into main Apr 15, 2026
8 checks passed
@lucifer1004 lucifer1004 deleted the feat/adr-gate branch April 15, 2026 05:09
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