Skip to content

replaceSection corrupts H2/H3 sections by silently appending duplicates #34

Description

@mebagwell

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):

## Foo
- new content

Actual:

## Foo
- original content

# Foo

- new content

A second identical call appends a second # Foo block. No error is raised at any point.

Defects

  1. H1-only heading match: /^#\s+/ matches only single-# headings, so H2 (##) and deeper sections are invisible to the matcher.
  2. 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".
  3. H1-only boundary detection: Section end detection also uses H1-only logic, causing incorrect boundaries even in rare H1-match cases.
  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions