Skip to content

Commit 81cb3ca

Browse files
committed
MDS020: harden required-structure diagnostics and test coverage
- checkBodySync now includes the expected value in its message so authors know immediately what text the body must contain - Add 7 unit tests covering wildcard heading level mismatch, soft-wrapped body sync, integer front matter values, sync not fired for absent headings, multiple simultaneously missing sections, and out-of-order sections that also carry a level mismatch - Add fixture tests: bad/wildcard-level.md (h2 where h1 required) and bad/multi-missing.md (two required sections both absent) - Update README Diagnostics table to show the improved body-sync format - Mark plan 61 complete https://claude.ai/code/session_01Lt7t1NZecqrysXAAp63LYa
1 parent 4e91011 commit 81cb3ca

8 files changed

Lines changed: 127 additions & 15 deletions

File tree

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ footer: |
1717
| ID | Status | Title |
1818
|-----|--------|------------------------------------------------------------------------------------------------------|
1919
| 52 || [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
20-
| 61 | 🔳 | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
20+
| 61 | | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
2121
| 65 || [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |
2222
| 78 || [Query subcommand for front-matter filtering](plan/78_query-command.md) |
2323
| 83 || [Security hardening batch](plan/83_security-hardening-batch.md) |

internal/rules/MDS020-required-structure/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ Describe the goal here.
221221
| extra section | unexpected section "## Extra" (expected "## Settings") |
222222
| out of order | section "## Tasks" out of order: expected after "## Goal" |
223223
| heading sync | heading does not match frontmatter: expected "MDS001" (from id), got "MDS002" |
224-
| body sync | body does not match frontmatter field "description" |
224+
| body sync | body does not match frontmatter field "description": expected "..." |
225225
| front matter schema | front matter does not satisfy schema CUE constraints: ... |
226226
| filename mismatch | filename "foo.md" does not match required pattern "[0-9]*_*.md" |
227227
| misplaced require | <?require?> is only recognized in schema files; this directive has no effect |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# ?
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
settings:
3+
schema: "../../internal/rules/MDS020-required-structure/bad/data/tmpl.md"
4+
diagnostics:
5+
- line: 1
6+
column: 1
7+
message: 'missing required section "## Goal"'
8+
- line: 1
9+
column: 1
10+
message: 'missing required section "## Tasks"'
11+
---
12+
# Title Only
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
settings:
3+
schema: "../../internal/rules/MDS020-required-structure/bad/data/wildcard-tmpl.md"
4+
diagnostics:
5+
- line: 1
6+
column: 1
7+
message: 'heading level mismatch for "Title": expected h1, got h2'
8+
---
9+
## Title

internal/rules/requiredstructure/rule.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1131,7 +1131,7 @@ func checkBodySync(
11311131
}
11321132

11331133
return []lint.Diagnostic{makeDiag(f.Path, dh.Line,
1134-
fmt.Sprintf("body does not match frontmatter field %q", field))}
1134+
fmt.Sprintf("body does not match frontmatter field %q: expected %q", field, expected))}
11351135
}
11361136

11371137
func validateCUESchemaSyntax(schema string) error {

internal/rules/requiredstructure/rule_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,3 +1450,93 @@ func TestExtractPIFileParam_MultiLine(t *testing.T) {
14501450
require.NoError(t, err)
14511451
assert.Equal(t, "other.md", result)
14521452
}
1453+
1454+
// =====================================================================
1455+
// Plan 61 hardening: additional edge-case tests
1456+
// =====================================================================
1457+
1458+
// Wildcard heading (`# ?`) must still enforce the correct level.
1459+
// A document h2 where h1 is required produces a level-mismatch diagnostic.
1460+
func TestCheck_WildcardHeadingLevelMismatch(t *testing.T) {
1461+
schemaPath := writeSchema(t, "# ?\n")
1462+
r := &Rule{Schema: schemaPath}
1463+
f := newTestFile(t, "doc.md", "## Title\n")
1464+
diags := r.Check(f)
1465+
expectDiagMsg(t, diags, `heading level mismatch for "Title": expected h1, got h2`)
1466+
}
1467+
1468+
// Soft-wrapped body paragraph (multiple lines joined by space) must
1469+
// match the front matter field value when concatenated.
1470+
func TestCheck_BodySyncSoftWrapped(t *testing.T) {
1471+
schemaPath := writeSchema(t, "# ?\n\n{description}\n")
1472+
r := &Rule{Schema: schemaPath}
1473+
f := newTestFile(t, "doc.md",
1474+
"---\ndescription: Line exceeds maximum length.\n---\n# My Rule\n\n"+
1475+
"Line exceeds\nmaximum length.\n")
1476+
diags := r.Check(f)
1477+
expectDiags(t, diags, 0)
1478+
}
1479+
1480+
// The improved body sync diagnostic must include the expected value so
1481+
// authors know what text to write.
1482+
func TestCheck_BodySyncDiagnosticIncludesExpected(t *testing.T) {
1483+
schemaPath := writeSchema(t, "# ?\n\n{description}\n")
1484+
r := &Rule{Schema: schemaPath}
1485+
f := newTestFile(t, "doc.md",
1486+
"---\ndescription: Correct description.\n---\n# My Rule\n\nWrong text.\n")
1487+
diags := r.Check(f)
1488+
expectDiagMsg(t, diags, `expected "Correct description."`)
1489+
}
1490+
1491+
// Integer front matter values are stringified for heading sync.
1492+
func TestCheck_SyncIntegerFrontMatterValue(t *testing.T) {
1493+
schemaPath := writeSchema(t, "# {id}: {name}\n")
1494+
r := &Rule{Schema: schemaPath}
1495+
f := newTestFile(t, "doc.md",
1496+
"---\nid: 42\nname: line-length\n---\n# 42: line-length\n")
1497+
diags := r.Check(f)
1498+
expectDiags(t, diags, 0)
1499+
}
1500+
1501+
// When a synced heading is absent from the document, checkSync must
1502+
// not emit a spurious diagnostic; only checkStructure reports it.
1503+
func TestCheck_SyncNotFiredForMissingHeading(t *testing.T) {
1504+
schemaPath := writeSchema(t, "# ?\n\n## {title}\n")
1505+
r := &Rule{Schema: schemaPath}
1506+
f := newTestFile(t, "doc.md",
1507+
"---\ntitle: My Section\n---\n# Title\n")
1508+
diags := r.Check(f)
1509+
// Exactly one diagnostic: missing required section, no sync error.
1510+
require.Len(t, diags, 1)
1511+
expectDiagMsg(t, diags, "missing required section")
1512+
for _, d := range diags {
1513+
assert.NotContains(t, d.Message, "sync")
1514+
}
1515+
}
1516+
1517+
// When several required sections are all absent, each gets its own
1518+
// "missing required section" diagnostic.
1519+
func TestCheck_MultipleMissingSections(t *testing.T) {
1520+
schemaPath := writeSchema(t,
1521+
"# ?\n\n## Goal\n\n## Tasks\n\n## Acceptance Criteria\n")
1522+
r := &Rule{Schema: schemaPath}
1523+
f := newTestFile(t, "doc.md", "# Title\n")
1524+
diags := r.Check(f)
1525+
expectDiagMsg(t, diags, `missing required section "## Goal"`)
1526+
expectDiagMsg(t, diags, `missing required section "## Tasks"`)
1527+
expectDiagMsg(t, diags, `missing required section "## Acceptance Criteria"`)
1528+
}
1529+
1530+
// A section that is both out of order AND at the wrong level must
1531+
// produce both the out-of-order and the level-mismatch diagnostic.
1532+
func TestCheck_OutOfOrderAlsoReportsLevelMismatch(t *testing.T) {
1533+
schemaPath := writeSchema(t,
1534+
"# ?\n\n## Goal\n\n## Tasks\n")
1535+
r := &Rule{Schema: schemaPath}
1536+
// Tasks (h2) appears before Goal; Goal appears at h3 (wrong level).
1537+
f := newTestFile(t, "doc.md",
1538+
"# Title\n\n## Tasks\n\n### Goal\n")
1539+
diags := r.Check(f)
1540+
expectDiagMsg(t, diags, `out of order`)
1541+
expectDiagMsg(t, diags, `heading level mismatch`)
1542+
}
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
id: 61
33
title: Required Structure Rule Hardening
4-
status: 🔳
4+
status:
55
---
66
# Required Structure Rule Hardening
77

@@ -13,28 +13,28 @@ with clear, actionable messages.
1313

1414
## Tasks
1515

16-
1. Audit current MDS020 behavior against `rules/proto.md`
16+
1. [x] Audit current MDS020 behavior against `rules/proto.md`
1717
and identify mismatch classes not covered by tests
1818
(heading order/level, optional sections, sync fields).
19-
2. Expand `requiredstructure` unit tests with focused fixtures
19+
2. [x] Expand `requiredstructure` unit tests with focused fixtures
2020
for false positives and false negatives, including
2121
front matter/body sync scenarios.
22-
3. Refine matching logic for required headings and sync points
22+
3. [x] Refine matching logic for required headings and sync points
2323
to reduce ambiguous comparisons and improve determinism.
24-
4. Improve diagnostic messages to include expected vs actual
24+
4. [x] Improve diagnostic messages to include expected vs actual
2525
structure details and precise heading context.
26-
5. Update `rules/MDS020-required-structure/README.md`
26+
5. [x] Update `rules/MDS020-required-structure/README.md`
2727
with clarified settings, examples, and diagnostics.
2828

2929
## Acceptance Criteria
3030

31-
- [ ] MDS020 correctly detects missing, reordered,
31+
- [x] MDS020 correctly detects missing, reordered,
3232
and wrong-level required headings from template input.
33-
- [ ] Sync checks correctly validate heading/body placeholders
33+
- [x] Sync checks correctly validate heading/body placeholders
3434
against front matter fields without spurious matches.
35-
- [ ] Diagnostics include actionable expected vs actual detail
35+
- [x] Diagnostics include actionable expected vs actual detail
3636
at stable line locations.
37-
- [ ] Tests cover representative success/failure cases,
37+
- [x] Tests cover representative success/failure cases,
3838
including template config edge cases.
39-
- [ ] All tests pass: `go test ./...`
40-
- [ ] `golangci-lint run` reports no issues
39+
- [x] All tests pass: `go test ./...`
40+
- [x] `golangci-lint run` reports no issues

0 commit comments

Comments
 (0)