Summary
The replaceSection() implementation behind replace_section MCP tool systematically corrupts concept bodies containing H2/H3 subsections. When a section title exists but is not an H1, the matcher fails silently and appends a new H1 section instead of replacing the intended content. Repeated calls produce unbounded duplicates with no error or warning.
Related-issue-distinction
This is not about schema validation or write-path constraints. It is a logic defect in how section boundaries are identified and replaced. No existing open issue addresses section duplication or H1-only matching behavior.
Observation
For concept bodies written with H2 subsections (including the app's own MCP agent output), calls to replaceSection targeting those sections always fail to match and instead create new H1 sections at the end of the file. The app's health checks (memory_status, bundle conformance) report no anomalies, so duplicates go undetected until manually grepped.
Reproduction (minimal)
Concept body (markdown, no H1 present):
## Foo
- original content
Call: replaceSection(body, "Foo", "- new content")
Expected (per the tool contract to replace the content under a heading):
Actual:
## Foo
- original content
# Foo
- new content
A second identical call appends a second # Foo block. No error is raised at any point.
Defects
- H1-only heading match:
/^#\s+/ matches only single-# headings, so H2 (##) and deeper sections are invisible to the matcher.
- Silent-append on no match: No error is thrown when a section cannot be located; instead, a new H1 section is appended, conflating "create new section" with "match failed".
- H1-only boundary detection: Section end detection also uses H1-only logic, causing incorrect boundaries even in rare H1-match cases.
- No duplicate detection: Multiple identical headings are not flagged; first-match-wins applies without ambiguity warnings.
Suggested fix
- Match any heading level:
/^#{1,6}\s+/ for both match and boundary detection.
- Section end should be the next heading at the SAME OR HIGHER level than the matched heading.
- On no match: either throw an error or make "create if missing" an explicit parameter.
- On multiple matches: throw an ambiguity error (a production hotfix using this logic has run since 2026-09-19 without regressions).
Environment
- Docker image
ghcr.io/thecodacus/understory:latest (no release tags; pulled September 2026)
- Node v22.23.1 (alpine)
- MCP transport: streamable-HTTP
Summary
The
replaceSection()implementation behindreplace_sectionMCP tool systematically corrupts concept bodies containing H2/H3 subsections. When a section title exists but is not an H1, the matcher fails silently and appends a new H1 section instead of replacing the intended content. Repeated calls produce unbounded duplicates with no error or warning.Related-issue-distinction
This is not about schema validation or write-path constraints. It is a logic defect in how section boundaries are identified and replaced. No existing open issue addresses section duplication or H1-only matching behavior.
Observation
For concept bodies written with H2 subsections (including the app's own MCP agent output), calls to
replaceSectiontargeting those sections always fail to match and instead create new H1 sections at the end of the file. The app's health checks (memory_status, bundle conformance) report no anomalies, so duplicates go undetected until manually grepped.Reproduction (minimal)
Concept body (markdown, no H1 present):
Call:
replaceSection(body, "Foo", "- new content")Expected (per the tool contract to replace the content under a heading):
Actual:
A second identical call appends a second
# Fooblock. No error is raised at any point.Defects
/^#\s+/matches only single-#headings, so H2 (##) and deeper sections are invisible to the matcher.Suggested fix
/^#{1,6}\s+/for both match and boundary detection.Environment
ghcr.io/thecodacus/understory:latest(no release tags; pulled September 2026)