From 2e0d30217859c5195ce483bd69128742f32b0263 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 8 Oct 2026 01:32:29 +0000 Subject: [PATCH] Treat image labels as Markdown alternative text --- docs/config.md | 4 ++- docs/roadmap/remediation.md | 9 +++++- internal/validation/markdown.go | 2 ++ internal/validation/markdown_check_test.go | 33 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 199bbd5..51db83c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -59,7 +59,9 @@ submodule contents remain out of scope under either setting. The parser recognizes inline links, images, reference links, and CommonMark autolinks; link-like text in code is ignored, and raw HTML destinations are -outside this check. Relative paths resolve from the source document in the +outside this check. For linked images, the image source and enclosing link +target are checked. Image labels supply alternative text. +Relative paths resolve from the source document in the captured commit. Query components are ignored for file existence, and percent escapes in local paths and fragments are decoded once. Fragments are compared in lowercase with anchors derived from visible ATX or Setext heading text. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index ff869c8..7f654b1 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -1,6 +1,6 @@ # ReleaseKit implementation remediation -Status: Two open v0.2.0 findings (8–9). Findings 1–7 are addressed. +Status: One open v0.2.0 finding (9). Findings 1–8 are addressed. The v0.2.0 review covers committed state `edd670e` and the implementation commits after `89981ab`, against the [implementation plan](implementation.md), @@ -330,6 +330,13 @@ Keep external destination availability outside this offline capability. ## 8. P2: Image alternative text creates additional release requirements +**Resolution:** Addressed. Destination extraction records an image source and +skips its alternative-text children. Regression coverage exercises nested +images/links, missing anchors, and outside-checkout label syntax under both +boundary settings. Real linked images still report missing sources and link +targets. The regression failed before the fix, and +`go test ./internal/validation` passed afterward. + **Owner:** [Markdown link extraction](../../internal/validation/markdown.go), `parseMarkdown`, especially the image branch and AST traversal at lines 65–110. diff --git a/internal/validation/markdown.go b/internal/validation/markdown.go index a32352a..48f95d3 100644 --- a/internal/validation/markdown.go +++ b/internal/validation/markdown.go @@ -82,6 +82,8 @@ func parseMarkdown(source []byte) *markdownDocument { Reference: node.Reference != nil, Location: markdownLocationAt(lineStarts, node.Pos()), }) + // Image labels contribute alternative text, not navigable destinations. + return ast.WalkSkipChildren, nil case *ast.AutoLink: document.links = append(document.links, markdownLink{ Destination: node.Destination.Value(source), diff --git a/internal/validation/markdown_check_test.go b/internal/validation/markdown_check_test.go index a9bb2d2..02aeb0a 100644 --- a/internal/validation/markdown_check_test.go +++ b/internal/validation/markdown_check_test.go @@ -46,6 +46,39 @@ func TestCheckMarkdownLinksResolvesCommittedDestinationsAndFragments(t *testing. } } +func TestCheckMarkdownLinksTreatsImageLabelsAsAlternativeText(t *testing.T) { + _, tree := committedMarkdownTree(t, map[string][]byte{ + "guide.md": []byte("" + + "![![inner](missing.png)](image.png)\n" + + "![[label](missing.md)](image.png)\n" + + "![[anchor](target.md#absent)](image.png)\n" + + "![[outside](../outside.md)](image.png)\n" + + "[![logo](image.png)](target.md#present)\n" + + "[![missing](absent.png)](absent.md)\n"), + "image.png": []byte("image"), + "target.md": []byte("# Present\n"), + }) + for _, enforceBoundary := range []bool{false, true} { + t.Run(fmt.Sprintf("boundary=%t", enforceBoundary), func(t *testing.T) { + err := checkMarkdownLinks(context.Background(), tree, enforceBoundary, nil) + var failure *Failure + if !errors.As(err, &failure) || failure.Check != MarkdownLinks || len(failure.Findings) != 2 { + t.Fatalf("error = %v, want only the missing linked-image source and link target", err) + } + want := map[string]bool{"absent.png": true, "absent.md": true} + for _, finding := range failure.Findings { + if !want[finding.Target] { + t.Errorf("unexpected destination finding: %#v", finding) + } + delete(want, finding.Target) + } + if len(want) != 0 { + t.Errorf("missing findings for real destinations: %v", want) + } + }) + } +} + func TestCheckMarkdownLinksReportsMissingPathsAnchorsAndCycles(t *testing.T) { _, tree := committedMarkdownTree(t, map[string][]byte{ "guide.md": []byte("" +