Documentation
¶
Overview ¶
Package markdown implements the byte-preserving Markdown engine that the agent-memory project uses to read and modify .agent-memory/ files without reformatting unchanged regions.
The pattern: parse the AST only to locate byte offsets, never use it to render output. Splices are byte-level substring replacements on the original source. See docs/patterns/byte-preserving-engine.md.
This package is the production version of spike S1. The spike code lives at spikes/s1-byte-preserving-markdown/.
Index ¶
- func AssignMissingIDs(src []byte) ([]byte, []string, error)
- func CountSections(src []byte) (int, error)
- func Merge3Way(base, ours, theirs []byte) (result []byte, conflicted bool, warnings []string, err error)
- func ReplaceSection(src []byte, sec Section, newContent []byte) ([]byte, error)
- func Splice(src []byte, ops []SpliceOp) ([]byte, error)
- func ValidateMarkdown(src []byte) error
- type Section
- type SpliceOp
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AssignMissingIDs ¶
AssignMissingIDs scans src for headings without an <!-- @id: ... --> anchor and injects one on the line immediately after each such heading. Returns the new bytes plus a slice of section IDs in document order (one entry per section; the value is the FINAL ID of that section, whether pre-existing or newly assigned).
Slug rules:
- lowercase
- alphanumeric only (a-z, 0-9); any other rune becomes a dash
- runs of dashes collapse to one
- leading and trailing dashes stripped
- truncated to 64 characters (then re-trimmed if the cut left a trailing dash)
- empty result falls back to "section" (then made unique)
Collisions are resolved by appending "-2", "-3", etc. Pre-existing anchors take precedence and reserve their ID even when a different heading would have produced the same natural slug.
AssignMissingIDs is idempotent: running it on its own output produces byte-identical bytes (the second pass sees every section already anchored and generates no splice ops).
Unchanged regions outside the inserted anchors are byte-identical to the input; the function reuses the byte-preserving Splice primitive.
func CountSections ¶
CountSections is a small convenience for callers that want to assert "this splice didn't accidentally drop a section" without re-doing a full ParseSections round trip themselves. Returns -1 on parse error.
func Merge3Way ¶
func Merge3Way(base, ours, theirs []byte) (result []byte, conflicted bool, warnings []string, err error)
Merge3Way performs a section-aware three-way merge of a memory Markdown file: base is the common ancestor, ours/theirs the two sides. It exists so a git merge driver can union two branches' edits to a shared `.agent-memory/` file instead of producing whole-file conflict markers.
The file is treated as a tree of sections keyed by their `@id` anchor (or, when un-anchored, by heading text + level + occurrence). The merge is recursive and byte-preserving — each kept section's bytes are copied verbatim from the side that owns them, so nothing is reflowed:
- present on both sides, unchanged or changed on only one side → take it;
- added on one side only → keep it (this is the common "both branches appended a different decision/pitfall" case → clean union);
- changed on BOTH sides differently → a `<!-- @merge-conflict -->` block wrapping both versions, and conflicted=true so the driver leaves the file unmerged for a human;
- deleted on one side → the surviving version is kept and a warning is recorded (memory is never silently lost; both-sides-deleted is honoured).
conflicted reports whether any section needs human resolution; warnings lists non-fatal notes (kept-despite-delete). A parse failure on any input returns an error so the caller can fall back to git's default merge.
func ReplaceSection ¶
ReplaceSection is a convenience wrapper that applies one Splice op covering the section's full range. Equivalent to:
Splice(src, []SpliceOp{{ByteStart: sec.ByteStart, ByteEnd: sec.ByteEnd, Replacement: newContent}})
func Splice ¶
Splice applies one or more SpliceOps to src and returns the new bytes.
All ByteStart/ByteEnd values refer to the ORIGINAL src; the function internally sorts ops by ByteStart and stitches the result piece by piece, so callers don't have to worry about offsets shifting between ops.
Returns an error if:
- any op has invalid bounds (ByteStart < 0, ByteEnd > len(src), ByteStart > ByteEnd);
- any two ops overlap (a previous op's ByteEnd > the next op's ByteStart after sorting).
An empty ops slice returns a copy of src and no error.
func ValidateMarkdown ¶
ValidateMarkdown is a thin post-splice sanity check. It parses src through goldmark and returns an error if the parser fails or returns a nil document. Goldmark accepts almost any input as best-effort Markdown, so this primarily catches programming errors (e.g., a splice that produced non-UTF-8 bytes by accident) rather than user-style malformed Markdown.
Heavier structural checks (per-category schema validation, required sections, etc.) live in internal/schema/ from T1.9 onward.
Types ¶
type Section ¶
type Section struct {
HeadingText string
HeadingLevel int
AnchorID string // empty if no <!-- @id: ... --> anchor follows the heading
Occurrence int // 1-based among sections with the same (HeadingText, HeadingLevel)
ByteStart int // inclusive; start of the heading line
ByteEnd int // exclusive; start of next heading at same/higher level, or len(src)
ContentHash string // "sha256:<hex>" of bytes [ByteStart, ByteEnd)
}
Section represents a Markdown section located in source bytes.
A "section" runs from a heading line (inclusive) up to the start of the next heading at the same or higher level — or to the end of the source if no such heading follows. Nested headings produce nested sections; a level-2 section therefore subsumes any level-3+ sections beneath it.
func FindByHeading ¶
FindByHeading returns a pointer to the section matching (text, level, occurrence), or (nil, false) if not found. Occurrence is 1-based; pass 1 for "first match".
func FindByID ¶
FindByID returns a pointer to the first section with the given anchor ID, or (nil, false) if not found.
func ParseSections ¶
ParseSections walks the goldmark AST and returns sections with byte offsets in document order. Headings inside fenced code blocks are not returned — goldmark recognises them as code and does not emit them as headings.
Every Section's ContentHash is computed against the current src, so a caller comparing hashes across writes detects byte-level drift.