Skip to content

Include enhancements: link adjustment and heading-level - #55

Merged
jeduden merged 17 commits into
mainfrom
plan-69
Mar 8, 2026
Merged

Include enhancements: link adjustment and heading-level#55
jeduden merged 17 commits into
mainfrom
plan-69

Conversation

@jeduden

@jeduden jeduden commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Automatic relative link/image rewriting when including files across directories
  • New heading-level: "absolute" parameter to shift included headings under the parent section
  • findParentHeadingLevel walks the AST to detect enclosing heading level
  • README examples for all configuration options (wrap, strip-frontmatter, heading-level, link rewriting)

Test plan

  • go test ./... — all 44 packages pass
  • go tool golangci-lint run — 0 issues
  • mdsmith check . — 0 diagnostics
  • Link adjustment edge cases: trailing slashes, fragments, query strings, images, nested brackets, mailto, absolute URLs
  • Heading shift with setext headings and code fences (13 test cases)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 7, 2026 20:46
jeduden and others added 2 commits March 7, 2026 21:49
Include directive (MDS021) now automatically rewrites relative link
and image targets so they resolve from the including file's directory.
New heading-level: "absolute" parameter shifts included headings to
nest under the enclosing section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show wrap, strip-frontmatter, heading-level, and link
rewriting usage in the MDS021 README examples section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

This PR extends the MDS021 include directive to (1) rewrite relative link/image targets in included content when files live in different directories, and (2) optionally shift included heading levels under the parent section via heading-level: "absolute".

Changes:

  • Add automatic relative link/image target rewriting for included content.
  • Add heading-level: "absolute" parameter support, including parent-heading detection and heading shifting.
  • Add/extend unit tests and update the MDS021 rule documentation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/rules/include/rule.go Validates heading-level, calls link rewriting + heading shifting during include generation, adds parent-heading detection helper.
internal/rules/include/rule_test.go Adds end-to-end tests for link adjustment, heading-level shifting, and combined behavior.
internal/rules/include/links.go Implements adjustLinks to rewrite Markdown link/image targets across directories.
internal/rules/include/links_test.go Adds unit tests covering link rewrite behavior (fragments, queries, images, skips).
internal/rules/include/headings.go Implements adjustHeadings to shift ATX/setext headings while skipping fenced code blocks.
internal/rules/include/headings_test.go Adds unit tests for heading shifting (ATX, setext, code fences, caps).
internal/rules/MDS021-include/README.md Documents new heading-level param and automatic link adjustment behavior.

Comment thread internal/rules/include/rule.go Outdated
Comment thread internal/rules/include/rule_test.go Outdated
The file parameter is FS-relative (rooted at the including file's
directory) while filePath is repo-root-relative. Normalize by
joining file with filePath's directory before calling adjustLinks.
Update tests to use subdirectory includes that exercise the rewrite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .github/CLAUDE.md copy and include it alongside
DEVELOPMENT.md in copilot-instructions.md. Both use
heading-level: "absolute" so headings nest under
# Copilot Instructions. Links in the CLAUDE.md copy
point to ../ so they resolve correctly from .github/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

internal/rules/include/rule.go:176

  • generateIncludeContent applies link rewriting and (optionally) heading shifting before applying wrap. When wrap is set, the included content is meant to be shown verbatim inside a code fence; rewriting links/headings beforehand can unexpectedly change the literal content being displayed. Consider skipping these transformations when wrap is present, or adding an explicit parameter to control whether rewriting/heading shifting applies to wrapped includes.
	// Rewrite relative links so they resolve from the including file's
	// directory. The file param is relative to f.FS (the including file's
	// directory), so join with filePath's directory to get a repo-root-
	// relative path matching filePath's coordinate system.
	includedPath := path.Join(path.Dir(filePath), file)
	text = adjustLinks(text, includedPath, filePath)

	// Shift headings when heading-level: "absolute" is set.
	if params["heading-level"] == "absolute" {
		parentLevel := findParentHeadingLevel(f, line)
		text = adjustHeadings(text, parentLevel)
	}

	// Wrap in code fence if requested.
	if wrap, ok := params["wrap"]; ok {
		text = "```" + wrap + "\n" + text
		if !strings.HasSuffix(text, "\n") {
			text += "\n"
		}
		text += "```\n"
	}

Comment thread internal/rules/include/links.go Outdated
Comment thread internal/rules/include/links.go
Comment thread internal/rules/include/headings.go
Comment thread internal/rules/include/headings.go Outdated
jeduden and others added 2 commits March 7, 2026 22:33
Propagate project root (parent of .mdsmith.yml) through Runner and
Fixer into lint.File.RootFS so include directives with ".." paths
resolve correctly. Delete .github/CLAUDE.md and .github/DEVELOPMENT.md
copies — copilot-instructions.md now uses file: ../CLAUDE.md and
file: ../DEVELOPMENT.md via the new traversal support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- adjustLinks now skips fenced code blocks and inline code spans so
  links in code examples are not rewritten
- codeFenceRe allows up to 3 leading spaces per CommonMark spec
- isClosingFence also respects up to 3 leading spaces
- atxRe now matches bare headings like "##" (no trailing space required)
- Added tests for all new behaviors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 07:07
jeduden and others added 2 commits March 8, 2026 08:12
The include directives themselves document the source files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Comment thread internal/rules/include/headings.go Outdated
Comment thread internal/rules/include/headings.go Outdated
Comment thread internal/rules/include/links.go Outdated
Comment thread internal/rules/include/rule.go
Comment thread internal/rules/MDS021-include/README.md Outdated
Comment thread internal/rules/include/rule_test.go Outdated
jeduden and others added 2 commits March 8, 2026 08:20
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Store only backtick/tilde run as fence marker (not leading spaces)
- Handle multi-backtick inline code spans in link rewriting
- Normalize filePath with filepath.ToSlash for Windows compatibility
- Remove stale ".." traversal diagnostic from README
- Rename TestCheck_DotDotTraversal to TestCheck_DotDotPathResolvesWithRootFS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 07:23
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment thread internal/rules/include/rule.go
Comment thread internal/rules/include/links.go Outdated
Comment thread internal/engine/runner.go
…RootFS

- Guard heading.Lines().Len() > 0 before accessing At(0)
- Capture exact fence opening run length; closing must be >= opening
- Set f.RootFS in RunSource when RootDir is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread .github/copilot-instructions.md Outdated
CLAUDE.md already links to DEVELOPMENT.md, so the separate include
is redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

internal/rules/include/rule.go:184

  • Link rewriting and heading shifting are applied before wrap handling, which means wrap: ... (code-fence includes) will still have its content mutated (links rewritten, headings shifted) even though the output is meant to be a literal code sample. That can break the intent of wrap (show the exact file contents). Consider skipping adjustLinks/adjustHeadings when wrap is set, or introducing an explicit opt-in/out parameter for these transformations.
	// Rewrite relative links so they resolve from the including file's
	// directory. The file param is relative to f.FS (the including file's
	// directory), so join with filePath's directory to get a repo-root-
	// relative path matching filePath's coordinate system.
	includedPath := path.Join(path.Dir(filePath), file)
	text = adjustLinks(text, includedPath, filePath)

	// Shift headings when heading-level: "absolute" is set.
	if params["heading-level"] == "absolute" {
		parentLevel := findParentHeadingLevel(f, line)
		text = adjustHeadings(text, parentLevel)
	}

	// Wrap in code fence if requested.
	if wrap, ok := params["wrap"]; ok {
		text = "```" + wrap + "\n" + text
		if !strings.HasSuffix(text, "\n") {
			text += "\n"
		}
		text += "```\n"
	}

internal/rules/include/rule.go:144

  • With the removal of the explicit ".." validation, invalid/escaping paths now fall through to fs.ReadFile and surface as include file ... not found: ... invalid argument, which is misleading and makes traversal issues harder to diagnose. Before calling fs.ReadFile, consider validating readPath (and/or resolvedFile) with fs.ValidPath and returning a dedicated diagnostic when the resolved path is invalid or escapes the project root.
	// Resolve file relative to the including file's directory.
	// Use RootFS (project root) when available so that paths
	// with ".." segments work across directories.
	resolvedFile := path.Join(path.Dir(filePath), file)
	readFS := f.FS
	readPath := file
	if f.RootFS != nil {
		readFS = f.RootFS
		readPath = resolvedFile
	}
	data, err := fs.ReadFile(readFS, readPath)
	if err != nil {
		return "", []lint.Diagnostic{makeDiag(filePath, line,
			fmt.Sprintf("include file %q not found: %v", file, err))}
	}

Comment thread internal/engine/runner.go
Comment thread .github/copilot-instructions.md Outdated
Comment thread internal/rules/include/links.go Outdated
Comment thread internal/rules/include/links.go
- Remove inline-code-level splitting from link rewriter so links with
  backticks in text (e.g. [`name`](target)) are matched correctly
- Skip rewriting targets containing whitespace (link titles)
- Fix RunSource comment to accurately describe FS/RootFS behavior
- .claude/settings.json link now correctly rewritten to ../

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread .github/copilot-instructions.md

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment thread internal/rules/include/rule.go
Comment thread internal/rules/include/links.go
Comment thread cmd/mdsmith/main.go Outdated
- Re-enable cross-file-reference-integrity for copilot-instructions.md
  now that link rewriting handles backticks in link text
- Add TestCheck_DotDotPathWithoutRootFS: clear diagnostic when ".."
  path is used but RootFS is not configured
- rootDirFromConfig falls back to cwd when no config file
- Emit diagnostic for ".." without RootFS instead of confusing error
- Fix adjustLinks doc comment to only mention fenced code blocks
- path.Clean file paths for consistent fs.FS access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread .mdsmith.yml
Align AGENTS.md overrides with .github/copilot-instructions.md so
both included-content files share the same linting settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 15:24

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment thread internal/rules/include/rule.go
Comment thread internal/rules/include/headings.go Outdated
Comment thread internal/rules/include/rule.go
- Replace strings.Contains(file, "..") with path-element check so
  filenames like "foo..bar.md" are not falsely rejected.
- Reject resolved paths that escape the project root when RootFS is set.
- Strip all leading whitespace before fence detection in headings.go so
  fenced code blocks inside list items are also skipped.
- Update diagnostics table in README.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

@jeduden
jeduden merged commit 50ae566 into main Mar 8, 2026
7 checks passed
@jeduden
jeduden deleted the plan-69 branch March 8, 2026 21:39
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.

2 participants