fix(productdocs): the ownership header is not a documentation section

docs/*.md is embedded and served to the model through the docs tool.
6b6a41d36 gave two documents the metadata header the documentation
standard requires. Markdown reads `---`, key lines, `---` as a setext
heading, so the tool listed "owner: @esengine / backup: @SivanCola / ..."
as a section of STUDIO_RELEASE.md and DOCS_STANDARD.md, beside an empty
title section. This was checked by reading both documents through the
tool.

The parser now blanks the header to spaces before parsing, so section
offsets and cited line ranges stay those of the file. An unterminated
delimiter is left as ordinary Markdown. STUDIO_RELEASE and DOCS_STANDARD
join the maintainer audience with RELEASING and the SignPath SOP, so a
user-facing search does not rank the release runbook.

The blanking lives in metadata.go: docs.go was at its 800-line budget.
This commit is contained in:
esengine committed 2026-09-16 21:01:33 -07:00
1 parent 6b6a41d367
commit c37dac3a40
3 files changed
+57 -2

No files matched your search

+2 -2
View File
@@ -558,7 +558,7 @@ func parseDocument(name, content string) *document {
}
func parseDocumentWithParser(name, content string, markdownParser parser.Parser) *document {
source := []byte(content)
source := blankMetadataHeader([]byte(content))
doc := &document{
path: path.Clean(name),
title: strings.TrimSuffix(strings.TrimSuffix(name, ".md"), ".zh-CN"),
@@ -762,7 +762,7 @@ func detectDocumentLanguage(name, content string) string {
func documentAudience(name string) string {
stem := strings.TrimSuffix(strings.TrimSuffix(name, ".md"), ".zh-CN")
switch strings.ToUpper(stem) {
case "RELEASING", "SIGNPATH_WINDOWS_ADMIN_SOP", "PRODUCTION_CHECKLIST", "THEME_ASSETS":
case "RELEASING", "SIGNPATH_WINDOWS_ADMIN_SOP", "PRODUCTION_CHECKLIST", "THEME_ASSETS", "STUDIO_RELEASE", "DOCS_STANDARD":
return "maintainer"
case "CHECKPOINTS", "GOAL_ENFORCEMENT", "SESSION_REFERENCE_ARCHITECTURE", "SPEC", "TASK_CONTRACT", "TOOL_CONTRACT":
return "developer"
+21
View File
@@ -392,6 +392,27 @@ func TestParserIgnoresHeadingsInsideFencedCode(t *testing.T) {
}
}
// The ownership header is metadata for maintainers. Markdown alone would read
// its closing --- as a setext underline and serve the owners as a section.
func TestOwnershipHeaderIsNotASection(t *testing.T) {
content := "---\nowner: @a\nbackup: @b\nstatus: active\nreviewed: 2026-09-17\n---\n\n# Runbook\n\n## Steps\n\nDo it.\n"
doc := parseDocument("RUNBOOK.md", content)
if len(doc.sections) != 2 {
t.Fatalf("sections = %d (%+v), want the title and Steps only", len(doc.sections), doc.sections)
}
for _, sec := range doc.sections {
if strings.Contains(sec.heading, "owner") || strings.Contains(sec.content, "@a") {
t.Fatalf("header leaked into section %q: %q", sec.heading, sec.content)
}
}
if doc.title != "Runbook" || doc.sections[1].startLine != 10 {
t.Fatalf("title=%q steps line=%d, want Runbook at the file's own line 10", doc.title, doc.sections[1].startLine)
}
if got := parseDocument("NOTE.md", "---\nnot closed\n# Title\n"); len(got.sections) == 0 {
t.Fatal("an unterminated delimiter is ordinary Markdown and must still parse")
}
}
func TestGoldmarkHeadingsPreserveTextAndSetextSemantics(t *testing.T) {
doc := parseDocument("EXAMPLE.md", "# Example\n\n### Configure *MCP* with `reasonix.toml`, C#, and <https://example.com>\n\nBody.\n\nSetext section\n--------------\n\nMore.\n")
if len(doc.sections) != 3 {
+34
View File
@@ -0,0 +1,34 @@
package productdocs
import "bytes"
// blankMetadataHeader turns the ownership header docs/DOCS_STANDARD.md requires
// into blank lines. Markdown reads `---`, key lines, `---` as a heading, which
// would surface owner names as a section; blanking keeps every offset and line.
func blankMetadataHeader(source []byte) []byte {
lines := bytes.SplitAfter(source, []byte("\n"))
if len(lines) == 0 || string(bytes.TrimSpace(lines[0])) != "---" {
return source
}
end := -1
for i := 1; i < len(lines); i++ {
if string(bytes.TrimSpace(lines[i])) == "---" {
end = i
break
}
}
if end < 0 {
return source
}
out := bytes.Clone(source)
offset := 0
for i := 0; i <= end; i++ {
for j := offset; j < offset+len(lines[i]); j++ {
if out[j] != '\n' && out[j] != '\r' {
out[j] = ' '
}
}
offset += len(lines[i])
}
return out
}