Skip to content

feat(MDS072): add external-link-check rule (issue #47)#707

Open
jeduden wants to merge 3 commits into
mainfrom
claude/youthful-knuth-r2pv0n
Open

feat(MDS072): add external-link-check rule (issue #47)#707
jeduden wants to merge 3 commits into
mainfrom
claude/youthful-knuth-r2pv0n

Conversation

@jeduden

@jeduden jeduden commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds MDS072 external-link-check — an opt-in rule (off by default) that validates external http:// and https:// URLs by making a HEAD request (fallback GET on 405), caching results per URL for the run, and reporting 4xx/5xx responses and transport errors as diagnostics. Resolves Add external URL link checking rule #47.
  • Creates plan/2606280208_external-link-check.md tracking the implementation.
  • Registers externallink in internal/rules/all/all.go.

Note on numbering: originally authored as MDS071, but MDS071 was taken by required-frontmatter on main (PR #733). Renumbered to MDS072 during rebase.

Changes

internal/rules/externallink/rule.go — MDS072 implementation:

  • Default off (EnabledByDefault() false); only activates when ApplySettings is called, so the alloc-budget gate sees the early-return path (0 allocs).
  • Reads three keys from the shared links: block: external-skip (regex patterns), external-timeout (default 5s), external-rate-limit (default 10 concurrent). Tolerates keys owned by MDS027 (site-root, validate-images, validate-reference-style) and MDS068 (style) so one shared links: block configures all three rules.
  • Package-level sync.Map cache (urlCache) ensures each unique URL is fetched at most once per process.
  • Semaphore channel (r.semaphore) caps in-flight requests to external-rate-limit.
  • sync.Once initializes the http.Client and semaphore lazily on first Check.

internal/rules/externallink/probe_net.go / probe_wasm.go — WASM build split: native HTTP probing lives behind a !(js && wasm) build tag; the wasm build carries a no-op prober so net/http never inflates the WebAssembly artifact.

internal/rules/externallink/rule_test.go / probe_net_test.go — unit tests using httptest.NewServer, covering the 200 / 404 / 405-then-GET / transport-error / cache-hit / autolink paths plus every ApplySettings branch (100% statement coverage of rule.go).

internal/rules/all/all.go — blank import to register MDS072.

plan/2606280208_external-link-check.md — plan file, status ✅.

Test plan

  • go build ./... — green
  • go test ./... — green
  • go tool golangci-lint run — 0 issues
  • go run ./cmd/mdsmith check . — 0 failures (550 files)
  • Alloc-budget gate: MDS072 returns nil when unconfigured (RateLimit zero) → 0 allocs/op, well under the ≤ 10 ceiling; pinned via perRuleAllocCeiling

🤖 Generated with Claude Code

https://claude.ai/code/session_01WPyEKiuZwHDey65F8yd8jK

@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.65%. Comparing base (3d1cf7a) to head (b896e6e).

Additional details and impacted files
Components Coverage Δ
Go 98.64% <100.00%> (+<0.01%) ⬆️
TypeScript 99.54% <ø> (ø)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeduden jeduden marked this pull request as ready for review June 29, 2026 18:34
Copilot AI review requested due to automatic review settings July 12, 2026 21:40
@jeduden jeduden force-pushed the claude/youthful-knuth-r2pv0n branch from db1565b to ef978a7 Compare July 12, 2026 21:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@jeduden jeduden changed the title feat(MDS071): add external-link-check rule (issue #47) feat(MDS072): add external-link-check rule (issue #47) Jul 12, 2026
@jeduden jeduden force-pushed the claude/youthful-knuth-r2pv0n branch from ef978a7 to f46eff6 Compare July 12, 2026 21:42
Opt-in rule (off by default) that probes external http/https URLs with an
HTTP HEAD request (falling back to GET on 405) and reports any URL that
returns a transport error, a 4xx, or a 5xx response. Results are cached
per URL for the run so a URL referenced in many files costs at most one
request. Resolves issue #47.

- AST walk collects inline links and autolinks; non-http(s) schemes and
  local destinations are skipped before any probe.
- Config via the shared links: block (external-skip, external-timeout,
  external-rate-limit); keys owned by MDS027/MDS068 are tolerated.
- WASM build split: probe_net.go (native HTTP) vs probe_wasm.go (no-op)
  keeps net/http out of the WebAssembly artifact.
- RateLimit==0 sentinel keeps an unconfigured rule at zero allocs so the
  alloc-budget gate can run it; a perRuleAllocCeiling entry pins it.

Numbered MDS072: MDS071 was taken by required-frontmatter on main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPyEKiuZwHDey65F8yd8jK
@jeduden jeduden force-pushed the claude/youthful-knuth-r2pv0n branch from f46eff6 to 648a4e3 Compare July 12, 2026 23:38
claude added 2 commits July 13, 2026 00:00
A three-angle code-review pass surfaced several defects in the first
MDS072 implementation, all fixed here with regression tests:

- Enable-with-`true` was a silent no-op: the bare `external-link-check:
  true` form leaves cfg.Settings nil, so ConfigureRule never calls
  ApplySettings and the RateLimit==0 "unconfigured" sentinel disabled
  the documented enable path. Defaults are now baked into the registered
  instance (newRule) so CloneInstance and the bare path inherit them,
  and the sentinel is gone. The network-bound alloc/timing gates skip
  the rule (isNetworkBound) instead of relying on the sentinel.
- Autolink diagnostics anchored at (1,1): an AutoLink has no walkable
  Text child, so position() now locates the literal <url> in the
  enclosing block's source for autolinks.
- external-rate-limit capped nothing: the semaphore was a per-Rule
  field, but the engine clones one Rule per worker; moved it (and the
  result cache) to package scope for a true global cap.
- Concurrent double-probe: added a singleflight.Group keyed by URL so
  concurrent workers share one request, restoring one-request-per-run.
- probe: shared client with per-request context timeouts, a bounded
  body drain for keep-alive, and defer-based slot release.
- Images (![alt](url)) are now probed alongside links and autolinks.

Regression tests: bare-enable end-to-end via checker.ConfigureEnabledRules
(internal/integration/externallink_enable_test.go), autolink position,
image probing, and concurrent single-request. Package coverage 100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPyEKiuZwHDey65F8yd8jK
Round 2 of the code-review pass found no functional defects; these are
documentation/comment accuracy touch-ups:

- Correct the isNetworkBound rationale: only the alloc-budget fixture
  carries an external URL; the per-rule bench doc does not, so the skip
  there is defensive.
- Document autolinkPosition's first-match behavior for duplicate
  identical autolinks (matches linkstyle; minor column-only inaccuracy).
- Expand the plan's known-limitations to cover the process-lifetime
  semaphore size freeze alongside the never-evicted cache (LSP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPyEKiuZwHDey65F8yd8jK
@jeduden jeduden requested a review from Copilot July 13, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Add external URL link checking rule

3 participants