diff --git a/lib/mix/statifier/adr_guard.ex b/lib/mix/statifier/adr_guard.ex index 5a9de254..3f4bad24 100644 --- a/lib/mix/statifier/adr_guard.ex +++ b/lib/mix/statifier/adr_guard.ex @@ -8,8 +8,17 @@ defmodule Mix.Statifier.AdrGuard do comments), and ADR-0058 (ADR number collisions). Each of the first five checks is a name or call-site pattern over the lines a diff adds - deliberately not an AST pass - so a false positive is cleared the way this - project already clears an Appendix D deviation: an inline comment on or - above the flagged line naming an ADR or the word "deviation". + project already clears an Appendix D deviation: an inline comment naming an + ADR or the word "deviation", on the flagged line itself or on the line + directly above it in the same hunk. + + That two-line window is the whole of what the guard reads, and it is kept + that narrow on purpose: a wider one would make the guard easier to satisfy + by accident, and widening it is a change to what the gate accepts, so + ADR-0011 makes it a human call. A citation two or more lines above the + flagged line clears nothing - for instance one written above a + `defp ... do` line whose first body line is the flagged call. Put the + citation on the flagged line, or on the line directly above it. ADR-0058's tree-local checks, `adr-0058-duplicate-number` and `adr-0058-readme-index`, are different in kind from all five: they are @@ -50,8 +59,10 @@ defmodule Mix.Statifier.AdrGuard do kind of line ADR-0018's Consequences show clearing itself by accident (a legitimate ADR citation that happens to sit beside an unrelated bead mention), so this check has its own marker, `ADR-0018-exempt`, and nothing - else clears it. Being line-based rather than AST-based, it can only tell a - comment/doc line from a code line by shape: a `#` line, a single-line + else clears it. The marker is read in the same two-line window: on the + flagged line or the line directly above it, and nowhere further up. Being + line-based rather than AST-based, it can only tell a comment/doc line from + a code line by shape: a `#` line, a single-line `@moduledoc`/`@doc`/`@typedoc "..."`, a `test "..." do` description, or a line inside a `\"""` doc heredoc. A doc heredoc's extent is read from the file's post-image text carried on `source` (populated by `collect/1`), not @@ -198,7 +209,8 @@ defmodule Mix.Statifier.AdrGuard do A finding is dropped when the line it fires on, or the line above it in the same hunk, cites an ADR number or the word "deviation" - the same inline - justification CLAUDE.md already asks for. + justification CLAUDE.md already asks for. A citation two or more lines + above the flagged line does not clear it (see the moduledoc). """ @spec analyze(source :: source()) :: [finding()] def analyze(source) do @@ -774,7 +786,8 @@ defmodule Mix.Statifier.AdrGuard do # Only added lines are checked: a check about what the code now does has # nothing to say about a line the diff removes. `previous` is the line above - # in the same hunk, which is where an inline citation usually sits. + # in the same hunk, and it is the only line above a flagged line that the + # escape checks read: the citation window is that line and the flagged one. defp parse_diff(diff) do {_path, _line, _previous, acc} = diff diff --git a/lib/mix/tasks/adr.check.ex b/lib/mix/tasks/adr.check.ex index 9fa6862f..e62a31f6 100644 --- a/lib/mix/tasks/adr.check.ex +++ b/lib/mix/tasks/adr.check.ex @@ -14,11 +14,13 @@ defmodule Mix.Tasks.Adr.Check do It runs as the `ADR guard` custom stage of `mix quality`, so drift from an accepted decision is a named gate failure rather than something review has to catch. Every finding names the ADR it is about. The ADR-0002/0003/0004/0008 - findings clear with an inline comment on or above the line citing that ADR or - the word "deviation" - the same justification CLAUDE.md already asks for. The - ADR-0018 bead-ID finding does not: an ADR citation is exactly what it is - checking against, so it clears only with its own marker, `ADR-0018-exempt`, - placed on or above the line. + findings clear with an inline comment citing that ADR or the word + "deviation", on the flagged line or on the line directly above it - the same + justification CLAUDE.md already asks for. The ADR-0018 bead-ID finding does + not: an ADR citation is exactly what it is checking against, so it clears + only with its own marker, `ADR-0018-exempt`, placed in the same place. A + citation two or more lines above the flagged line clears neither kind of + finding. The ADR-0058 numbering findings (`adr-0058-duplicate-number`, `adr-0058-readme-index`, `adr-0058-base-number`) clear on neither escape diff --git a/test/mix/statifier/adr_guard_test.exs b/test/mix/statifier/adr_guard_test.exs index 5ba604cb..57af44c2 100644 --- a/test/mix/statifier/adr_guard_test.exs +++ b/test/mix/statifier/adr_guard_test.exs @@ -216,6 +216,21 @@ defmodule Mix.Statifier.AdrGuardTest do ]) == [] end + # The window is the flagged line and the one directly above it, on + # purpose: a citation above a function head does not reach the flagged + # call in its body. Widening the window is a change to what the gate + # accepts (ADR-0011), not a fix. + # sabotage: have parse_line/2 carry every earlier line of the hunk in + # `previous`, widening the window -> red + test "a citation two lines above the flagged line does not clear it" do + assert [%{line: 12, check: "adr-0003-effects"}] = + analyze("lib/statifier/document.ex", [ + " # ADR-0003: deviation, the reply is awaited in place", + " defp await_reply(ref) do", + " receive do" + ]) + end + # sabotage: loosen @escape_pattern to any `#` comment -> red test "a comment that cites nothing clears nothing" do assert [%{check: "adr-0003-effects"}] = @@ -460,6 +475,19 @@ defmodule Mix.Statifier.AdrGuardTest do ]) == [] end + # The marker is read in the same window as an ADR citation, and no + # further up. + # sabotage: have parse_line/2 carry every earlier line of the hunk in + # `previous`, widening the window -> red + test "the ADR-0018-exempt marker two lines above does not clear the finding" do + assert [%{line: 12, check: "adr-0018-bead-id"}] = + analyze("lib/statifier/document.ex", [ + " # ADR-0018-exempt: named for git blame", + " value = compute(input)", + " # st-af3 replaces this function's body" + ]) + end + # This is the exact accident ADR-0018's Consequences document at e2524fc: # a correct, unrelated ADR citation must not clear a bead-ID finding. # sabotage: have bead_cited?/1 call the shared cited?/1 (@escape_pattern)