textpatch

package
v1.113.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package textpatch applies anchored edits to a text document and reports what changed, without knowing what kind of document it is. Prompts, assets, and (later) knowledge pages all hand it a body and get a body back, so one edit grammar serves every content kind.

The grammar is "find this exact text, put that text in its place": an anchor must resolve to exactly one span unless the caller opts into an occurrence, and an anchor that matches nothing or matches ambiguously aborts the whole call rather than applying a guess. Line numbers are read output only and are never accepted as an edit anchor, because a line number is stale the moment an earlier edit lands.

Index

Constants

View Source
const (
	// DefaultContextBytes is how much surrounding text a match reports.
	DefaultContextBytes = 160
	// DefaultLocateLimit caps reported matches.
	DefaultLocateLimit = 20
)

Locate defaults.

View Source
const (
	FieldSizeBytes = "size_bytes"
	FieldLines     = "lines"
	FieldSections  = "sections"
	FieldLandmarks = "landmarks"
	FieldHash      = "hash"
	FieldContent   = "content"
	FieldSection   = "section"
	FieldCount     = "count"
	FieldMatches   = "matches"
	FieldTruncated = "truncated"
	FieldEdits     = "edits"
	FieldDiff      = "diff"
)

Response field names. The verbs answer identically on every tool that adopts the grammar, so the wire keys live here beside the argument schema rather than being re-declared per toolkit, where they could drift apart.

View Source
const (
	// OpReplace swaps the span matched by Find (literal) or Pattern (regex)
	// for Replace. An empty Replace deletes the matched span.
	OpReplace = "replace"
	// OpInsertBefore places Text immediately before the matched anchor,
	// leaving the anchor in place.
	OpInsertBefore = "insert_before"
	// OpInsertAfter places Text immediately after the matched anchor,
	// leaving the anchor in place.
	OpInsertAfter = "insert_after"
	// OpReplaceSection replaces a whole markdown section (its heading
	// through the next heading of the same or higher level) with Text.
	OpReplaceSection = "replace_section"
	// OpMoveSection relocates a whole section before or after another
	// heading, or to the start or end of the document.
	OpMoveSection = "move_section"
	// OpAppend adds Text at the end of the body; no anchor needed.
	OpAppend = "append"
	// OpPrepend adds Text at the start of the body; no anchor needed.
	OpPrepend = "prepend"
)

Edit operations. An edit with no Op is a Replace.

View Source
const (
	OccurrenceFirst = "first"
	OccurrenceLast  = "last"
	OccurrenceAll   = "all"
)

Occurrence selectors accepted by Edit.Occurrence in addition to a 1-based decimal index. Absent means "the anchor must be unique".

View Source
const (
	PositionStart = "start"
	PositionEnd   = "end"
)

Move destinations accepted by Edit.Position for OpMoveSection.

View Source
const (
	CodeNoMatch         = "PATCH_NO_MATCH"
	CodeAmbiguous       = "PATCH_AMBIGUOUS"
	CodeStaleBase       = "PATCH_STALE_BASE"
	CodeNotText         = "PATCH_NOT_TEXT"
	CodeTooLarge        = "PATCH_TOO_LARGE"
	CodeSectionNotFound = "PATCH_SECTION_NOT_FOUND"
	CodeBadPattern      = "PATCH_BAD_PATTERN"
	CodeBadEdit         = "PATCH_BAD_EDIT"
	// CodeBadSelector reports a CSS selector that does not parse.
	CodeBadSelector = "PATCH_BAD_SELECTOR"
	// CodeNoStructure reports a document whose content type has no addressable
	// structure, so section- and selector-scoped verbs do not apply to it.
	CodeNoStructure = "PATCH_NO_STRUCTURE"
	// CodeUnresolvedMarkup reports markup that could not be resolved into a
	// reliable element tree, so no element span was trusted.
	CodeUnresolvedMarkup = "PATCH_UNRESOLVED_MARKUP"
)

Error codes. Every failure an agent can correct carries one of these, so the caller can branch without parsing prose.

View Source
const (
	// DefaultMaxEdits caps how many edits one call may carry.
	DefaultMaxEdits = 100
	// DefaultMaxPatternLen caps the length of a regex source string. RE2
	// match time is linear, so this bounds compilation cost rather than
	// guarding against catastrophic backtracking.
	DefaultMaxPatternLen = 512
	// DefaultMaxMatches caps how many spans a single occurrence:"all" edit
	// may touch.
	DefaultMaxMatches = 1000
)

Limits bounding a single Apply call. Zero means the package default.

View Source
const PropertiesJSON = `` /* 5206-byte string literal not displayed */

PropertiesJSON is the JSON Schema fragment for the patch, search, and navigation arguments. Every tool that adopts the grammar splices these exact properties into its own input schema, so the grammar an agent reads on manage_asset is literally the grammar it reads on manage_prompt.

View Source
const VerbsDescription = "To change part of existing content, use 'patch' with anchored edits; do not read the whole " +
	"body back and resend it. To find the part to change, use 'locate' (literal or regex search reporting line " +
	"numbers, enclosing section, and a copyable context window) or 'outline' (heading tree with sizes). " +
	"'get_content' reads the whole body, one section, or a line range; 'stats' reports size, line count, version, " +
	"and content hash without any body; 'diff' compares two versions. Anchors are text, never line numbers: an " +
	"anchor that matches nothing or matches ambiguously refuses the whole call and writes nothing. The 'content' " +
	"argument remains the way to do a genuine full rewrite."

VerbsDescription is the shared steering text every tool that adopts the grammar appends to its own description, so the rule reads identically wherever an agent meets it.

Variables

This section is empty.

Functions

func AddProperties

func AddProperties(props map[string]any)

AddProperties splices the shared grammar into a tool's schema properties.

A name collision panics rather than resolving silently: the whole point of the shared fragment is that the grammar reads identically on every tool, so a tool redefining one of these names is an authoring error that must surface at build time, not a divergence to be tolerated at runtime.

func ContentFields

func ContentFields(body string, req ContentRequest) (map[string]any, error)

ContentFields renders a requested span of the document alongside the whole document's size and line count, so a caller reading one section still learns how much it did not read. The resolved heading is reported only for a section read.

func CountLines

func CountLines(body string) int

CountLines returns the number of lines in body. An empty body has zero lines; a body not ending in a newline still counts its final partial line.

func LineRange

func LineRange(body string, start, end int) (string, error)

LineRange returns lines [start, end] of body, 1-based and inclusive. end may exceed the line count, in which case the rest of the body is returned.

It resolves the two byte offsets directly rather than materializing the document's lines, so reading ten lines out of a large asset stays cheap.

func LocateFields

func LocateFields(body string, q LocateQuery, opts Options) (map[string]any, error)

LocateFields renders every match of an anchor with the total count, which is the number that tells an agent whether the anchor is safe to patch with.

func OutlineFields

func OutlineFields(body string, syntax Syntax) map[string]any

OutlineFields renders the heading tree plus the document's size and line count. The section list is never nil, so it serializes as [] not null. On HTML syntax it also reports the addressable landmarks (elements carrying an id or a data-* marker), which is the whole answer for a JSX dashboard with no headings.

func PatchFields

func PatchFields(res Result) map[string]any

PatchFields renders the outcome of an applied or dry-run patch: the per-edit report and a unified diff of the changed hunks, never the new body.

func PropertiesMap

func PropertiesMap() map[string]any

PropertiesMap returns PropertiesJSON decoded, for tools whose input schema is built as a Go map rather than raw JSON. It panics on a malformed constant, which is a build-time authoring error rather than a runtime condition.

func StatsFields

func StatsFields(body string) map[string]any

StatsFields renders size, line count, and content hash, with none of the body.

func UnifiedDiff

func UnifiedDiff(oldBody, newBody string, context int) string

UnifiedDiff renders the changed hunks between two bodies in unified diff format, with context unchanged lines around each hunk (0 uses the default). It returns "" when the bodies are identical.

Unified diff is the platform's output format for a change and never an input format: as output the server generates it from two known strings, which makes it the most compact accurate description of what changed and the one a model reads most fluently.

func UnifiedDiffLabeled

func UnifiedDiffLabeled(oldBody, newBody, oldLabel, newLabel string, context int) string

UnifiedDiffLabeled is UnifiedDiff with the conventional ---/+++ file header, used when comparing two named versions.

Types

type ContentRequest

type ContentRequest struct {
	Syntax     Syntax
	Section    string
	Selector   string
	Occurrence string
	LineStart  int
	LineEnd    int
}

ContentRequest selects which span of a document to read: the whole body when nothing is set, one named section or selector-addressed element, or an inclusive 1-based line range. Syntax names the region grammar for the section and selector forms.

type Edit

type Edit struct {
	Op         string `json:"op,omitempty"`
	Find       string `json:"find,omitempty"`
	Pattern    string `json:"pattern,omitempty"`
	Replace    string `json:"replace,omitempty"`
	Text       string `json:"text,omitempty"`
	Section    string `json:"section,omitempty"`
	Selector   string `json:"selector,omitempty"`
	Occurrence string `json:"occurrence,omitempty"`
	Before     string `json:"before,omitempty"`
	After      string `json:"after,omitempty"`
	Position   string `json:"position,omitempty"`
}

Edit is one anchored change. Field use depends on Op:

replace          Find or Pattern -> Replace (empty Replace deletes)
insert_before    Find or Pattern, Text placed before the anchor
insert_after     Find or Pattern, Text placed after the anchor
replace_section  Section -> Text
move_section     Section, plus Before, After, or Position
append/prepend   Text only

Section additionally scopes the anchor search on replace, insert_before, and insert_after, which is how a phrase repeated across a document becomes unambiguous without quoting a long anchor. Selector does the same on an HTML, JSX or SVG document, naming an element by CSS selector; it is the precise form (a balanced element span) where section is the convenient one (a heading).

type EditResult

type EditResult struct {
	// Index is the edit's position in the request list.
	Index int `json:"index"`
	// Op is the effective operation that ran.
	Op string `json:"op"`
	// Matches is how many spans the edit changed (1 unless occurrence:"all").
	Matches int `json:"matches"`
	// Normalized is true when the exact anchor failed and the edit resolved
	// only after CRLF and trailing-whitespace normalization.
	Normalized bool `json:"normalized,omitempty"`
	// Line is the 1-based line in the pre-edit body where the change landed.
	Line int `json:"line"`
}

EditResult reports the outcome of one applied edit. It deliberately carries no content: the point of patching is that the body never crosses the wire.

type Error

type Error struct {
	Code string
	// EditIndex is the 0-based index of the failing edit, or -1 when the
	// failure is not attributable to one edit.
	EditIndex int
	Message   string
	Hint      string
}

Error is a corrective, self-describing patch failure. Code is stable and machine-readable; Hint tells the agent how to recover.

func NotTextError

func NotTextError(contentType string) *Error

NotTextError refuses a verb that only makes sense on text against binary content, rather than corrupting it or dumping it as garbage.

func StaleBaseError

func StaleBaseError(base, current int) *Error

StaleBaseError refuses a patch whose base_version no longer matches the stored content, so a concurrent edit is never silently overwritten.

func (*Error) Error

func (e *Error) Error() string

Error implements error, naming the failing edit when there is one.

type Landmark added in v1.113.0

type Landmark struct {
	// Tag is the element's tag name as written.
	Tag string `json:"tag"`
	// Selector is a selector that resolves to this element, ready to paste into
	// a patch.
	Selector string `json:"selector"`
	// ID is the element's id attribute when it has one.
	ID string `json:"id,omitempty"`
	// Line is the 1-based line of the element's start tag.
	Line int `json:"line"`
	// SizeBytes is the byte size of the element's whole outer span.
	SizeBytes int `json:"size_bytes"`
}

Landmark is an addressable element an agent would target on an HTML, JSX or SVG document: one carrying an id or a data-* region marker. It reports a copyable selector, so outline answers "where can I patch" for a dashboard that has no headings at all.

type LocateQuery

type LocateQuery struct {
	// Find is a literal anchor; Pattern is a regex. Exactly one is required.
	Find    string
	Pattern string
	// Section scopes the search to one section when set; Selector scopes it to
	// one selector-addressed element on an HTML document. Occurrence
	// disambiguates a selector that matches several elements.
	Section    string
	Selector   string
	Occurrence string
	// ContextBytes is how much text to include around each hit; 0 uses
	// DefaultContextBytes.
	ContextBytes int
	// Limit caps the reported matches; 0 uses DefaultLocateLimit. Count
	// always reports the true total up to the match cap.
	Limit int
}

LocateQuery selects what to search for and how much of each hit to report.

type LocateResult

type LocateResult struct {
	// Count is the number of matches found, up to the match cap. Truncated
	// reports whether the scan or the reported list stopped short.
	Count   int     `json:"count"`
	Matches []Match `json:"matches"`
	// Truncated is true when more hits exist than the reported list carries,
	// either because Limit cut the list or the match cap stopped the scan.
	Truncated bool `json:"truncated,omitempty"`
}

LocateResult reports every hit for a search, and how many there were in total, which is the number that tells an agent whether an anchor is safe.

func Locate

func Locate(body string, q LocateQuery, opts Options) (LocateResult, error)

Locate finds every occurrence of a literal or regex anchor and reports where each one sits, what section encloses it, and a context window wide enough to copy verbatim into a patch anchor.

The count is the point: an agent that locates first never hits PATCH_AMBIGUOUS, and an agent that wanted to change every occurrence learns how many there are before deciding.

type Match

type Match struct {
	// Line is the 1-based line the match starts on.
	Line int `json:"line"`
	// Offset is the byte offset of the match in the document.
	Offset int `json:"offset"`
	// Section is the heading of the innermost section containing the match,
	// empty when the match sits above the first heading.
	Section string `json:"section,omitempty"`
	// Context is the surrounding text, wide enough to paste into a "find"
	// anchor when the match alone would be ambiguous.
	Context string `json:"context"`
}

Match is one hit reported by Locate: enough to copy a verbatim anchor out of the context window without reading the document.

type Options

type Options struct {
	// Syntax is the region grammar for section- and selector-scoped edits. The
	// zero value is markdown, so an Options that sets no syntax keeps the
	// markdown behavior #1033 shipped.
	Syntax Syntax
	// MaxEdits caps the number of edits; 0 uses DefaultMaxEdits.
	MaxEdits int
	// MaxPatternLen caps regex source length; 0 uses DefaultMaxPatternLen.
	MaxPatternLen int
	// MaxMatches caps spans touched by one occurrence:"all" edit; 0 uses
	// DefaultMaxMatches.
	MaxMatches int
	// MaxResultBytes rejects a result body larger than this; 0 means no cap.
	MaxResultBytes int
	// DiffContext is the number of unchanged context lines around each diff
	// hunk; 0 uses defaultDiffContext.
	DiffContext int
}

Options bound one Apply call. A zero Options uses the package defaults and imposes no result-size limit.

type Result

type Result struct {
	// Body is the patched document.
	Body string `json:"-"`
	// Edits reports each edit in request order.
	Edits []EditResult `json:"edits"`
	// Diff is a unified diff of the changed hunks only.
	Diff string `json:"diff"`
	// SizeBytes is the length of the patched body.
	SizeBytes int `json:"size_bytes"`
	// Lines is the line count of the patched body.
	Lines int `json:"lines"`
}

Result is the outcome of an Apply call.

func Apply

func Apply(body string, edits []Edit, opts Options) (Result, error)

Apply runs an ordered list of edits against body and returns the patched document with a per-edit report and a unified diff of the changed hunks.

Every edit resolves against an in-memory copy and the first failure aborts the whole call, so a partially applied patch is never written. Edits apply in order against the evolving body, which means a later edit may anchor on text an earlier edit introduced.

type Section

type Section struct {
	// Heading is the full heading line as written ("## Methodology").
	Heading string `json:"heading"`
	// Title is the heading text with its hashes and spacing removed.
	Title string `json:"title"`
	// Path is the ancestor chain ("Report > Methodology"), which
	// disambiguates a title that repeats under different parents.
	Path string `json:"path"`
	// Level is 1 for "#" through 6 for "######".
	Level int `json:"level"`
	// Line is the 1-based line of the heading.
	Line int `json:"line"`
	// SizeBytes is the byte size of the whole section, heading included.
	SizeBytes int `json:"size_bytes"`
	// contains filtered or unexported fields
}

Section is one markdown heading and the span it owns: the heading line through the line before the next heading of the same or higher level.

func Content

func Content(body string, req ContentRequest) (string, Section, error)

Content returns the requested span of body. The returned Section is populated only for a region read, so a caller can report which heading or element it resolved to.

func FindSection

func FindSection(body, name string, editIndex int) (Section, error)

FindSection resolves a caller-supplied markdown section name against body's outline. The name may be the heading line ("## Methodology"), the bare title ("Methodology"), or an ancestor path ("Report > Methodology"); matching is case-insensitive. A name that matches nothing or more than one section is an error carrying the outline or the disambiguating advice.

func Outline

func Outline(body string, syntax Syntax) []Section

Outline returns the heading tree of body in document order, never nil. For markdown syntax the headings are ATX headings, with those inside fenced code blocks ignored; for HTML syntax they are the <h1>..<h6> elements; a structureless document has none.

func SectionText

func SectionText(body, name string) (string, Section, error)

SectionText returns the text of the named markdown section, heading included.

type Stats

type Stats struct {
	SizeBytes int    `json:"size_bytes"`
	Lines     int    `json:"lines"`
	Hash      string `json:"hash"`
}

Stats is the cheap metadata of a document: enough to decide whether to read it at all, without reading any of it.

func DocStats

func DocStats(body string) Stats

DocStats reports the size, line count, and content hash of a body. The hash is sha256 hex, so a caller can tell whether a document changed without transferring it. Callers that need only the size or the line count should use len and CountLines, which do not hash.

type Syntax added in v1.113.0

type Syntax int

Syntax is how a document's regions are named. It is derived from the content type, not guessed from the bytes, so the same body addressed as markdown and as HTML answers to different region grammars deliberately.

const (
	// SyntaxMarkdown names regions by ATX heading. It is the zero value, so a
	// caller that sets no syntax keeps the markdown behavior #1033 shipped.
	SyntaxMarkdown Syntax = iota
	// SyntaxHTML names regions by HTML heading or by CSS selector, over an
	// element tree with balanced spans. It serves HTML, JSX, SVG and XML.
	SyntaxHTML
	// SyntaxNone has no addressable structure; only anchored edits apply.
	SyntaxNone
)

func SyntaxForContentType added in v1.113.0

func SyntaxForContentType(ct string) Syntax

SyntaxForContentType selects the region grammar for a media type, routing through pkg/contenttype so the platform's single detection seam decides. HTML, JSX, SVG and XML get the element grammar; markdown and plain text keep ATX headings; every other textual type has no structure.

Directories

Path Synopsis
Package patchmcp renders textpatch failures as MCP tool results carrying the platform's error contract.
Package patchmcp renders textpatch failures as MCP tool results carrying the platform's error contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL