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
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ footer: |
| ID | Status | Title |
|-----|--------|-----------------------------------------------------------------------------------------------------------------|
| 50 | 🔲 | [Redundancy / Duplication Detection](plan/50_redundancy-duplication-detection.md) |
| 51 | 🔲 | [Section-Level Size Limits](plan/51_section-level-size-limits.md) |
| 51 | | [Section-Level Size Limits](plan/51_section-level-size-limits.md) |
| 52 | 🔲 | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
| 53 | ⛔ | [Conciseness Scoring](plan/53_conciseness-scoring.md) |
| 54 | ⛔ | [Conciseness Metrics Design and Implementation](plan/54_metrics-guide-tradeoffs.md) |
Expand Down
1 change: 1 addition & 0 deletions cmd/mdsmith/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
_ "github.com/jeduden/mdsmith/internal/rules/linelength"
_ "github.com/jeduden/mdsmith/internal/rules/listindent"
_ "github.com/jeduden/mdsmith/internal/rules/maxfilelength"
_ "github.com/jeduden/mdsmith/internal/rules/maxsectionlength"
_ "github.com/jeduden/mdsmith/internal/rules/nobareurls"
_ "github.com/jeduden/mdsmith/internal/rules/noduplicateheadings"
_ "github.com/jeduden/mdsmith/internal/rules/noemphasisasheading"
Expand Down
1 change: 1 addition & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
_ "github.com/jeduden/mdsmith/internal/rules/linelength"
_ "github.com/jeduden/mdsmith/internal/rules/listindent"
_ "github.com/jeduden/mdsmith/internal/rules/maxfilelength"
_ "github.com/jeduden/mdsmith/internal/rules/maxsectionlength"
_ "github.com/jeduden/mdsmith/internal/rules/nobareurls"
_ "github.com/jeduden/mdsmith/internal/rules/noduplicateheadings"
_ "github.com/jeduden/mdsmith/internal/rules/noemphasisasheading"
Expand Down
1 change: 1 addition & 0 deletions internal/engine/categories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
_ "github.com/jeduden/mdsmith/internal/rules/headingstyle"
_ "github.com/jeduden/mdsmith/internal/rules/linelength"
_ "github.com/jeduden/mdsmith/internal/rules/listindent"
_ "github.com/jeduden/mdsmith/internal/rules/maxsectionlength"
_ "github.com/jeduden/mdsmith/internal/rules/nobareurls"
_ "github.com/jeduden/mdsmith/internal/rules/noduplicateheadings"
_ "github.com/jeduden/mdsmith/internal/rules/noemphasisasheading"
Expand Down
24 changes: 24 additions & 0 deletions internal/integration/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
_ "github.com/jeduden/mdsmith/internal/rules/linelength"
_ "github.com/jeduden/mdsmith/internal/rules/listindent"
_ "github.com/jeduden/mdsmith/internal/rules/maxfilelength"
_ "github.com/jeduden/mdsmith/internal/rules/maxsectionlength"
_ "github.com/jeduden/mdsmith/internal/rules/nobareurls"
_ "github.com/jeduden/mdsmith/internal/rules/noduplicateheadings"
_ "github.com/jeduden/mdsmith/internal/rules/noemphasisasheading"
Expand Down Expand Up @@ -129,6 +130,7 @@ func applySettingsToRule(
}

func TestRuleFixtures(t *testing.T) {
primeDirectoryStructureWarnOnce(t)
dirs := discoverFixtureDirs(t)

for _, dir := range dirs {
Expand Down Expand Up @@ -376,6 +378,28 @@ func runFixSingleFile(

// --- shared helpers ---

// primeDirectoryStructureWarnOnce fires MDS033's "no allowed patterns"
// sync.Once warning up front. Without this, test cleanup in
// applySettingsToRule leaves MDS033 in a configured-but-empty state,
// so the warning would fire the first time any post-MDS033 fixture
// calls checkAllRules, producing a spurious diagnostic.
func primeDirectoryStructureWarnOnce(t *testing.T) {
t.Helper()
r := rule.ByID("MDS033")
if r == nil {
return
}
cr, ok := r.(rule.Configurable)
if !ok {
return
}
require.NoError(t, cr.ApplySettings(map[string]any{"allowed": []any{}}))
f, err := lint.NewFile("prime.md", []byte("# x\n"))
require.NoError(t, err)
_ = r.Check(f)
require.NoError(t, cr.ApplySettings(cr.DefaultSettings()))
}

func discoverFixtureDirs(t *testing.T) []string {
t.Helper()
dirs, err := filepath.Glob("../../internal/rules/MDS*-*")
Expand Down
109 changes: 109 additions & 0 deletions internal/rules/MDS036-max-section-length/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
id: MDS036
name: max-section-length
status: ready
description: Section length must not exceed per-level or per-heading limits.
---
# MDS036: max-section-length

Section length must not exceed per-level or per-heading limits.

- **ID**: MDS036
- **Name**: `max-section-length`
- **Status**: ready
- **Default**: disabled
- **Fixable**: no
- **Implementation**:
[source](./)
- **Category**: heading

A section spans from its heading line up to (but not including) the next
heading line of any level, or the end of file. Nested subsections are
measured separately from their parent, so the limit applies to the
content written directly under each heading.

## Settings

| Setting | Type | Default | Description |
|---------------|------|---------|----------------------------------------------------|
| `max` | int | 0 | Default line limit; zero disables the global rule. |
| `per-level` | map | `{}` | Map from heading level (1-6) to line limit. |
| `per-heading` | list | `[]` | Regex patterns with per-heading line limits. |

Lookup order for a heading: `per-heading` (first matching regex wins),
then `per-level`, then `max`. A resolved limit of zero disables the
check for that heading.

## Config

Enable with a default limit:

```yaml
rules:
max-section-length:
max: 100
```

Per-level and per-heading overrides:

```yaml
rules:
max-section-length:
max: 100
per-level:
1: 200
2: 80
per-heading:
- pattern: "^Changelog$"
max: 500
```
Comment on lines +47 to +59

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

The rule supports per-heading (regex-based) limits, but there are no integration fixture files exercising per-heading settings. Consider adding at least one good/ and one bad/ fixture that uses per-heading to validate YAML/front-matter decoding and end-to-end behavior (similar to the existing default/per-level fixtures).

Copilot uses AI. Check for mistakes.

Disable:

```yaml
rules:
max-section-length: false
```

## Examples

### Good

<?include
file: good/default.md
wrap: markdown
?>

```markdown
# Title

Short section within the limit.

## Subsection

Also short. Each section is bounded by the next heading of any level.

## Another

Stays under the limit.
```

<?/include?>

### Bad

<?include
file: bad/default.md
wrap: markdown
?>

```markdown
# Too Long

line a
line b
line c
line d
```

<?/include?>
14 changes: 14 additions & 0 deletions internal/rules/MDS036-max-section-length/bad/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
settings:
max: 3
diagnostics:
- line: 1
column: 1
message: 'section "# Too Long" too long (6 > 3)'
---
# Too Long

line a
line b
line c
line d
17 changes: 17 additions & 0 deletions internal/rules/MDS036-max-section-length/bad/per-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
settings:
max: 0
per-level:
2: 2
diagnostics:
- line: 3
column: 1
message: 'section "## Overflowing" too long (5 > 2)'
---
# Overview

## Overflowing

a
b
c
15 changes: 15 additions & 0 deletions internal/rules/MDS036-max-section-length/good/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
settings:
max: 10
---
# Title

Short section within the limit.

## Subsection

Also short. Each section is bounded by the next heading of any level.

## Another

Stays under the limit.
18 changes: 18 additions & 0 deletions internal/rules/MDS036-max-section-length/good/per-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
settings:
max: 0
per-level:
2: 4
---
# Overview

Top-level has no limit because `max` is zero and level 1 is not listed.
Only level-2 sections must stay within four lines.

## Short

Two lines of text.

## Also Short

Two more lines.
1 change: 1 addition & 0 deletions internal/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ row: "| [{id}]({filename}) | `{name}` | {status} | {description} |"
| [MDS031](MDS031-unclosed-code-block/README.md) | `unclosed-code-block` | ready | Fenced code blocks must have a closing fence delimiter. |
| [MDS032](MDS032-no-empty-alt-text/README.md) | `no-empty-alt-text` | ready | Images must have non-empty alt text for accessibility. |
| [MDS033](MDS033-directory-structure/README.md) | `directory-structure` | ready | Markdown files must exist only in explicitly allowed directories. |
| [MDS036](MDS036-max-section-length/README.md) | `max-section-length` | ready | Section length must not exceed per-level or per-heading limits. |
<?/catalog?>
Loading
Loading