Conversation
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>
There was a problem hiding this comment.
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. |
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>
There was a problem hiding this comment.
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. Whenwrapis 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 whenwrapis 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"
}
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>
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>
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…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>
CLAUDE.md already links to DEVELOPMENT.md, so the separate include is redundant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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
wraphandling, which meanswrap: ...(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 ofwrap(show the exact file contents). Consider skippingadjustLinks/adjustHeadingswhenwrapis 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 tofs.ReadFileand surface asinclude file ... not found: ... invalid argument, which is misleading and makes traversal issues harder to diagnose. Before callingfs.ReadFile, consider validatingreadPath(and/orresolvedFile) withfs.ValidPathand 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))}
}
- 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>
- 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>
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>
- 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>
Summary
heading-level: "absolute"parameter to shift included headings under the parent sectionfindParentHeadingLevelwalks the AST to detect enclosing heading levelTest plan
go test ./...— all 44 packages passgo tool golangci-lint run— 0 issuesmdsmith check .— 0 diagnostics🤖 Generated with Claude Code