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
25 changes: 24 additions & 1 deletion internal/actrace/actrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ var (
// statusLineRE matches a `**Status:**` line (also `- **Status**:`),
// capturing the rest of the line, mirroring internal/tools/adrstatus.
statusLineRE = regexp.MustCompile(`(?i)^[-*\s]*\*\*status:?\*\*:?\s*(.*)$`)
// headRE matches a `## Status` heading on its own line. The status value
// then lives on the next non-empty line, not inline. Mirrors
// internal/tools/adrstatus, which supported both the `**Status:**` line and
// the `## Status` heading form; an ADR using the heading form must classify
// the same way it did before the extraction.
headRE = regexp.MustCompile(`(?i)^#{1,6}\s+status\s*$`)
// adrCiteRe captures a bare-text `ADR-NNNN` reference and the two characters
// that follow, so a linked `[ADR-0042](../adr/...)` (followed by "](") can be
// excluded — those stay covered by `task docs-check`.
Expand Down Expand Up @@ -987,11 +993,28 @@ func loadADRStatusByNumber(root string) (map[int]string, error) {
if readErr != nil {
return nil, fmt.Errorf("reading %s: %w", base, readErr)
}
for _, ln := range strings.Split(string(b), "\n") {
// A present ADR file always gets an entry. The original adrstatus.parseADR
// defaulted an unrecognized status to "unknown" rather than omitting the
// ADR; the backward gate treats a missing entry as a missing ADR (a
// violation), so omitting a present-but-statusless ADR would wrongly flag
// its TestADR_* pins. "unknown" is not retired, so it is not a violation.
byNum[n] = "unknown"
lines := strings.Split(string(b), "\n")
for i, ln := range lines {
if m := statusLineRE.FindStringSubmatch(ln); m != nil {
byNum[n] = classifyStatus(m[1])
break
}
// `## Status` heading: classify the next non-empty line.
if headRE.MatchString(ln) {
for _, next := range lines[i+1:] {
if w := strings.TrimSpace(next); w != "" {
byNum[n] = classifyStatus(w)
break
}
}
break
}
}
}
return byNum, nil
Expand Down
41 changes: 41 additions & 0 deletions internal/actrace/actrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,47 @@ func TestLoadSupersessorByNumber_ParsesRetiredStatusLines(t *testing.T) {
assert.Equal(t, want, got, "retired ADR number → supersessor number")
}

func TestLoadADRStatusByNumber_HandlesBothStatusForms(t *testing.T) {
t.Parallel()

root := t.TempDir()
adrDir := filepath.Join(root, "docs", "adr")
require.NoError(t, os.MkdirAll(adrDir, 0o755))
write := func(name, body string) {
require.NoError(t, os.WriteFile(filepath.Join(adrDir, name), []byte(body), 0o644))
}

// The common `**Status:**` bold-line form.
write("0042-bold-line.md",
"# ADR-0042\n\n**Status:** Accepted, 2026-01-01.\n")
// The `## Status` heading form, value on the next non-empty line. Both
// internal/tools/adrstatus and this package must classify it the same way;
// dropping this branch silently read such an ADR as missing and flagged its
// TestADR_* pins (the ADR-0043 regression).
write("0043-heading-form.md",
"# ADR-0043: A decision\n\n## Status\n\nAccepted\n\n## Context\n")
// A retired ADR via the heading form classifies as superseded, not accepted.
write("0006-retired-heading.md",
"# ADR-0006\n\n## Status\n\n~~accepted~~. Superseded by ADR-0043.\n")
// A present ADR with no recognizable status form gets a "unknown" entry, not
// no entry. A missing entry reads as a missing ADR in the backward gate, so
// omitting a present file would wrongly flag its TestADR_* pins. Mirrors the
// original adrstatus.parseADR default.
write("0099-no-status.md",
"# ADR-0099: A decision with no status section\n\n## Context\n\nText.\n")

got, err := loadADRStatusByNumber(root)
require.NoError(t, err)

want := map[int]string{
42: "accepted",
43: "accepted",
6: "superseded",
99: "unknown",
}
assert.Equal(t, want, got, "ADR number → classified status across both status forms")
}

func TestParseACs_FindsVerifyAfterWrappedCriterion(t *testing.T) {
t.Parallel()
// The criterion text wraps across lines, so the verify: sub-line is not
Expand Down