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 |
|-----|--------|------------------------------------------------------------------------------------------------------|
| 52 | ✅ | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
| 61 | 🔳 | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
| 61 | | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
| 65 | ✅ | [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |
| 78 | ✅ | [Query subcommand for front-matter filtering](plan/78_query-command.md) |
| 83 | ✅ | [Security hardening batch](plan/83_security-hardening-batch.md) |
Expand Down
2 changes: 1 addition & 1 deletion internal/rules/MDS020-required-structure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ Describe the goal here.
| extra section | unexpected section "## Extra" (expected "## Settings") |
| out of order | section "## Tasks" out of order: expected after "## Goal" |
| heading sync | heading does not match frontmatter: expected "MDS001" (from id), got "MDS002" |
| body sync | body does not match frontmatter field "description" |
| body sync | body does not match frontmatter field "description": expected "..." |
| front matter schema | front matter does not satisfy schema CUE constraints: ... |
| filename mismatch | filename "foo.md" does not match required pattern "[0-9]*_*.md" |
| misplaced require | <?require?> is only recognized in schema files; this directive has no effect |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# ?
12 changes: 12 additions & 0 deletions internal/rules/MDS020-required-structure/bad/multi-missing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
settings:
schema: "../../internal/rules/MDS020-required-structure/bad/data/tmpl.md"
diagnostics:
- line: 1
column: 1
message: 'missing required section "## Goal"'
- line: 1
column: 1
message: 'missing required section "## Tasks"'
---
# Title Only
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
settings:
schema: "../../internal/rules/MDS020-required-structure/bad/data/wildcard-tmpl.md"
diagnostics:
- line: 1
column: 1
message: 'heading level mismatch for "Title": expected h1, got h2'
---
## Title
2 changes: 1 addition & 1 deletion internal/rules/requiredstructure/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ func checkBodySync(
}

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

func validateCUESchemaSyntax(schema string) error {
Expand Down
90 changes: 90 additions & 0 deletions internal/rules/requiredstructure/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1450,3 +1450,93 @@ func TestExtractPIFileParam_MultiLine(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "other.md", result)
}

// =====================================================================
// Plan 61 hardening: additional edge-case tests
// =====================================================================

// Wildcard heading (`# ?`) must still enforce the correct level.
// A document h2 where h1 is required produces a level-mismatch diagnostic.
func TestCheck_WildcardHeadingLevelMismatch(t *testing.T) {
schemaPath := writeSchema(t, "# ?\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md", "## Title\n")
diags := r.Check(f)
expectDiagMsg(t, diags, `heading level mismatch for "Title": expected h1, got h2`)
}

// Soft-wrapped body paragraph (multiple lines joined by space) must
// match the front matter field value when concatenated.
func TestCheck_BodySyncSoftWrapped(t *testing.T) {
schemaPath := writeSchema(t, "# ?\n\n{description}\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md",
"---\ndescription: Line exceeds maximum length.\n---\n# My Rule\n\n"+
"Line exceeds\nmaximum length.\n")
diags := r.Check(f)
expectDiags(t, diags, 0)
}

// The improved body sync diagnostic must include the expected value so
// authors know what text to write.
func TestCheck_BodySyncDiagnosticIncludesExpected(t *testing.T) {
schemaPath := writeSchema(t, "# ?\n\n{description}\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md",
"---\ndescription: Correct description.\n---\n# My Rule\n\nWrong text.\n")
diags := r.Check(f)
expectDiagMsg(t, diags, `expected "Correct description."`)
}

// Integer front matter values are stringified for heading sync.
func TestCheck_SyncIntegerFrontMatterValue(t *testing.T) {
schemaPath := writeSchema(t, "# {id}: {name}\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md",
"---\nid: 42\nname: line-length\n---\n# 42: line-length\n")
diags := r.Check(f)
expectDiags(t, diags, 0)
}

// When a synced heading is absent from the document, checkSync must
// not emit a spurious diagnostic; only checkStructure reports it.
func TestCheck_SyncNotFiredForMissingHeading(t *testing.T) {
schemaPath := writeSchema(t, "# ?\n\n## {title}\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md",
"---\ntitle: My Section\n---\n# Title\n")
diags := r.Check(f)
// Exactly one diagnostic: missing required section, no sync error.
require.Len(t, diags, 1)
expectDiagMsg(t, diags, "missing required section")
for _, d := range diags {
assert.NotContains(t, d.Message, "sync")
}
}

// When several required sections are all absent, each gets its own
// "missing required section" diagnostic.
func TestCheck_MultipleMissingSections(t *testing.T) {
schemaPath := writeSchema(t,
"# ?\n\n## Goal\n\n## Tasks\n\n## Acceptance Criteria\n")
r := &Rule{Schema: schemaPath}
f := newTestFile(t, "doc.md", "# Title\n")
diags := r.Check(f)
expectDiagMsg(t, diags, `missing required section "## Goal"`)
expectDiagMsg(t, diags, `missing required section "## Tasks"`)
expectDiagMsg(t, diags, `missing required section "## Acceptance Criteria"`)
}

// A section that is both out of order AND at the wrong level must
// produce both the out-of-order and the level-mismatch diagnostic.
func TestCheck_OutOfOrderAlsoReportsLevelMismatch(t *testing.T) {
schemaPath := writeSchema(t,
"# ?\n\n## Goal\n\n## Tasks\n")
r := &Rule{Schema: schemaPath}
// Tasks (h2) appears before Goal; Goal appears at h3 (wrong level).
f := newTestFile(t, "doc.md",
"# Title\n\n## Tasks\n\n### Goal\n")
diags := r.Check(f)
expectDiagMsg(t, diags, `out of order`)
expectDiagMsg(t, diags, `heading level mismatch`)
}
24 changes: 12 additions & 12 deletions plan/61_required-structure-hardening.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
id: 61
title: Required Structure Rule Hardening
status: 🔳
status:
---
# Required Structure Rule Hardening

Expand All @@ -13,28 +13,28 @@ with clear, actionable messages.

## Tasks

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

## Acceptance Criteria

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