Source
Documentation

Sections & addressing

usesource://usesource/docs/sections.md

Every markdown file in Source is a section tree. A heading of rank n opens a section that owns everything after it — paragraphs, code, lower-ranked headings — until the next heading of rank ≤ n. Agents read and edit at section granularity, so updating one runbook step never rewrites the file around it.

Take this document at /runbooks/deploy.md:

Deploy notes for the api service.

# Deploy

## Prepare

Check the dashboard before starting.

## Ship

Run the release script.

### Rollback

Revert the release commit.

Deploy owns everything below it; Ship owns Rollback; the sentence before the first heading is the preamble — a section of its own.

Addresses#

Every section has two addresses, and every operation that takes a ref accepts either:

  • Slug path — GitHub-style slugs of the heading trail: deploy/ship/rollback. Stable across reorderings; this is the address to store.
  • Ordinal — 1-based document position, nesting by rank: 1.2.1 here is deploy/ship/rollback. Convenient for "read section 4" style access.

The preamble is "" (or ordinal 0). Duplicate headings get -2, -3 suffixes, deduplicated per sibling group, not per document — so two ## Notes under different parents keep their clean slugs, and an exact slug path always wins over an ambiguous bare slug.

list_sections returns the whole address book of a file:

[
  { "ordinal": "0", "slugPath": "", "title": "", "rank": 0 },
  { "ordinal": "1", "slugPath": "deploy", "title": "Deploy", "rank": 1 },
  { "ordinal": "1.1", "slugPath": "deploy/prepare", "title": "Prepare", "rank": 2 },
  { "ordinal": "1.2", "slugPath": "deploy/ship", "title": "Ship", "rank": 2 },
  { "ordinal": "1.2.1", "slugPath": "deploy/ship/rollback", "title": "Rollback", "rank": 3 }
]

These same addresses are the heading anchors in the web viewer — #deploy/ship on the page and ref: "deploy/ship" in the API are one namespace.

read_section#

read_section returns a section as two fields — its own heading line, and the body beneath it with subsections included:

{ "libraryId": "LIB_ID", "path": "/runbooks/deploy.md", "ref": "deploy/ship" }
{
  "heading": "## Ship",
  "body": "\nRun the release script.\n\n### Rollback\n\nRevert the release commit.\n",
  "sections": [ ... ]
}

The split is the point: body is exactly what replace_section takes as body, so read → edit → write is lossless. A single content string carrying both would put the heading back into the payload — and an agent that wrote it back would duplicate it.

replace_section#

replace_section writes either part, or both. Whatever you omit is kept, and every other byte of the document stays identical:

{
  "libraryId": "LIB_ID",
  "path": "/runbooks/deploy.md",
  "ref": "deploy/ship",
  "body": "\nRun the release script with --canary first.\n\n### Rollback\n\nRevert the release commit.\n"
}

After the call, only the Ship subtree changed:

## Ship

Run the release script with --canary first.

### Rollback

Revert the release commit.

Send heading instead and the section is renamed without its body ever crossing the wire — the response returns the new slugPath, because the address moved with the title. Send both and the section is rewritten atomically. Sending neither is an error, not a no-op — and so is anything that instructs no change, such as heading: "" on the preamble, which has no heading line to begin with.

A heading is whatever the file uses: an ATX line (## Install), or a setext heading — a title over a row of = or -, so at least two lines and more when the title is soft-wrapped. read_section returns setext headings as those source lines and replace_section accepts them back, so the round trip holds and a setext section can be renamed like any other. A setext underline binds to the text directly above it, so writing one where the line above is prose would swallow the section; replace_section inserts a blank line to keep the heading standing on its own, and says so in a notice.

Writing several sections in one call is supported. A heading in body of the same rank as the section closes it and opens a sibling after it — that is how to append a section, or split a long one in two. A heading of higher rank (fewer #) opens a new section that also takes in the sections that follow it as its subsections. Each created section is reported as a heading_escaped notice naming its address, every section whose address moved as a section_address_moved, and the response's slugPath still addresses the section you wrote. Confirm the structure is what you meant by re-reading it with list_sections, or read_section on the affected sections — there is no need to fall back to write_file and re-send the whole document.

Writes are applied and reported, never silently reinterpreted. Every write answers with a notices array — always present, empty when there is nothing to report — and each entry is an object a caller can branch on, not a sentence to parse:

{
  "code": "section_address_moved",
  "message": "Section address changed: \"notes\" is now \"notes-1\" — use the new slugPath in later calls.",
  "previousRef": "notes",
  "ref": "notes-1"
}

code says what happened, ref is the address the notice is about in the document as it now stands, and previousRef is the address it had before the write. There is one entry per fact: a write that moves three addresses returns three entries, never one sentence naming all three.

Three families of code. Consequence codes report what this edit did to the rest of the document: a section the write created, because body held a heading of the section's rank or higher, with ref naming it (heading_escaped), a rank change that absorbs the sections below (heading_rank_changed), an emptied body (body_emptied), another section's address moving (section_address_moved), or another section ceasing to exist (section_destroyed). The address moves matter most: renaming one of two ## Notes siblings shifts the -1 suffix on the other, so a stored address can start resolving to a different section. A destroyed section carries previousRef and no ref — there is no address left that names it, and inventing one would be worse than saying nothing.

Validity codes report whether the document produced is sound markdown, and whether this viewer will render it as written — a different question from what the edit did. There are seven. unclosed_code_fence is a fenced block that is never closed, so everything after it becomes part of the code block; it carries the opening fence's line. literal_escape_sequence is a line whose prose holds a backslash the markdown parse left literal, immediately followed by n, t or r, so the reader sees the two characters \n, \t or \r rather than a line break, a tab or a carriage return. Only prose is examined — code blocks, inline code, raw HTML, link and image destinations and titles, image alt text and autolinks never are — and a backslash escaped as \\ is not reported, which is how to keep one you meant; an unescaped one is reported whatever was meant, so C:\new is. It is one entry per line, its message naming which sequences the line holds, how many of each, and both repairs. The other five are render-fidelity codes: conditions under which well-formed markdown will not render as you meant, because of what this viewer's render path does to it — facts no general markdown linter can name. raw_html_block_dropped and raw_html_inline_dropped report a raw HTML node the viewer drops before rendering: a block (a <div> on its own line) goes with everything inside it, an inline tag (<kbd>) goes while the text around it stays, one entry per tag; a tag inside a code block is code, and an HTML comment is not a deletion of content, so neither is reported. url_scheme_stripped reports a link, image or link reference definition whose URL scheme the viewer does not keep in that position — javascript: on a link, data: on an image — so the URL is stripped and the text shown unlinked or the image empty; the entry names the schemes kept there. mermaid_unknown_diagram_type and mermaid_syntax_error report a mermaid fence the renderer cannot parse — the first when its opening line names no diagram type it knows, the second when a known type's source does not parse, with line the source line the renderer named as a line of the written document. A fence is checked by parsing it with the same renderer the viewer draws with, never by laying it out, and a fence the check could not be run against yields nothing rather than a guess. Every validity code carries line and ref, and every one is a warning and never a rejection — the document holds exactly the bytes you sent, and nothing is repaired, because where you meant to close the fence, or what you meant the HTML to be, is a guess. A validity finding is scoped to the content this write supplied: the section replace_section wrote, the body replace_in_section edited, the whole document for write_file. A defect already in that content is reported even though this write did not introduce it; one elsewhere in the document is not. So an empty notices does not mean the document is sound — it means nothing was found in what you just wrote.

Borrowed codes are markdownlint's own, not ours. An entry whose code has the form MD### was produced by markdownlint itself, running over the document this write stored — so it means exactly what markdownlint's page for that rule says, and info.url on the entry links that page for the version that ran, so the link comes from the finding rather than from a copy kept here. Four rules run, and the bar for admitting one is that the condition makes the document render wrong, not that it offends a style guide: MD011 (a reversed link, (text)[url], which renders as plain text), MD042 (an empty link, [x](), which renders as nothing), MD052 (a reference link whose [label]: definition is missing, which renders as literal brackets) and MD056 (a table row whose cell count differs from its header's, which silently drops data). Line length, trailing whitespace and heading increments are style opinions and are off. Borrowed entries carry line and ref like the rest, plus info — info.fix relays the repair markdownlint described, which nothing here applies. They are scoped to the content this write supplied, exactly as the validity codes are, and they are warnings in the same way: the document holds the bytes you sent. Nothing else in markdownlint's catalogue runs, so an empty notices is not a claim about those rules either.

All of it is derived by re-parsing the written document, never predicted from the input, so a ## heading inside a fenced code block never triggers a notice. The code set may grow: treat one you do not recognize as informational and process the rest of the response.

The slugPath in the response always addresses the section that was just written. It is never a guess: a write that could not leave the addressed section standing is refused rather than reported as a success pointing somewhere else.

replace_in_section#

replace_in_section runs a regex find-and-replace scoped to one section — the smallest possible edit:

{
  "libraryId": "LIB_ID",
  "path": "/runbooks/deploy.md",
  "ref": "deploy/prepare",
  "pattern": "the dashboard",
  "replacement": "the release dashboard"
}

The result reports count, the number of replacements made, and the same coded notices array — a replacement that rewrites a subsection's heading line renames it, and a section_address_moved entry carries its old and new address; zero matches leaves the file untouched and returns an empty notices. Its validity findings cover the whole body it edited, not only the spans the pattern matched: a fence's opening line and its missing closer can fall on either side of a replacement. A match elsewhere in the document — the same phrase under Ship, say — is out of scope and unharmed. Matching covers the whole body, subsection heading lines included; only the section's own heading line is out of reach.

pattern is a regular expression, and flags defaults to g. Every match is replaced unless you send flags: "", which replaces only the first. Because pattern is a regex and not literal text, | is alternation: a markdown table row sent unescaped matches far more than the row, and an empty alternative matches at every position.

Every call is bounded by a replacement ceiling — maxReplacements when you send it, otherwise 10. The replacements a call would make are counted after the version check and before anything is written. A call over the ceiling is refused whole with too_many_replacements (409 on REST): nothing is stored, no version is bumped, no replacement is applied, and the refusal says how many it would have made and which ceiling applied:

{
  "code": "too_many_replacements",
  "message": "This call would make 131 replacements, more than the ceiling of 10, so nothing was written. …",
  "replacements": 131,
  "maxReplacements": 10
}

If every one of them is meant, re-send the same call with maxReplacements set to the reported replacements; otherwise narrow the pattern. The rule is the same with or without the field — apply at or under the ceiling, refuse over it — so a maxReplacements sent in advance still refuses a pattern that reaches further than you declared. It is an integer of at least 1. Replacements are bounded, not matches: a call without g makes at most one replacement and is never refused, and neither is a call that matches nothing.

pattern is the source of a JavaScript regular expression. The characters \ ^ $ . | ? * + ( ) [ ] { } carry meaning, and a literal one is written with a backslash before it. A literal backslash is therefore \\ in the pattern — and in a JSON request body each of those backslashes is escaped again, so it is four characters inside the JSON string. An over- or under-escaped pattern usually still compiles, so it is not rejected: it matches something other than what you meant. flags is a JavaScript RegExp flag string of at most 10 characters, read as the engine reads it.

replacement is a template, not plain text. The engine interprets these tokens, and only these:

Token Inserts
$$ a literal $ — the way to write a dollar sign
$& the matched text
$` the text of the section's body before the match
$' the text of the section's body after the match
$n, $nn the numbered capture group
$<name> the named capture group

A two-digit $nn is one group only when the pattern has that many groups, and is otherwise $n followed by a literal digit: $29 against a pattern with two groups writes group 2 and then 9. A $n naming no group the pattern has — $0 included — is written as its own characters, and so is $<name> when the pattern has no named group; a $<name> naming none of the pattern's named groups inserts nothing. The table-append idiom — capture the last row as (…\n) and send $1 followed by the new row — works exactly as it always has.

Every interpreted token is reported. An applied call returns one replacement_token_expanded notice for each distinct token it interpreted, $$ included, however many matches it was expanded in:

{
  "code": "replacement_token_expanded",
  "message": "The replacement token $2 was interpreted, not written as its own characters: on the first match it expanded to \"HOLDER\", and it was expanded in 1 match. 'replacement' is a template; '$$' writes a literal '$'.",
  "ref": "pricing/team"
}

The message names the token, quotes its expansion on the first match — the first 80 characters, with the full length stated, when it is longer — and says how many matches it was expanded in; ref is the written section. These entries come first, in the order the tokens appear in replacement, and are never capped. A call that matches nothing, a refused call and a replacement with no interpreted token carry none.

Every other content field is literal. write_file's content and replace_section's heading and body carry no template or regular-expression syntax: no $ token and no backslash in them is interpreted, and they are stored exactly as sent. To write text containing $1 into a section without a pattern, use replace_section.

Comments#

A comment is a remark on one section, with an ordered list of replies. People and agents add them with add_comment and answer them with add_reply; a comment is archived or it is not, and archiving is the only way to take it out of view.

The reads you already make hand you the comments. read_file carries the open comments on the document and read_section those on the section and its subsections — at most 20, oldest first, with commentsTotal and commentsTruncated saying how many there are; list_comments returns the rest. list_sections carries a commentCount per section. Each comment carries its own version, and writtenAgainst: the document version it was written against, so you can tell whether the text has changed since.

A write can close the comment about it. write_file, replace_section and replace_in_section accept up to 10 comment operations in comments, applied together with the write or not at all:

{
  "libraryId": "LIB_ID",
  "path": "/runbooks/deploy.md",
  "ref": "deploy/ship",
  "version": "SECTION_VERSION",
  "body": "\nRun the release script with --canary first.\n",
  "comments": [
    { "op": "archive", "commentId": "COMMENT_ID", "version": "COMMENT_VERSION" },
    { "op": "add", "sectionRef": "deploy/ship", "body": "Canary step added." }
  ]
}

The response's commentResults says what each operation did, in order. If the comment gained a reply since you read it, the whole write is refused with version_conflict naming the comment — read the reply first. The comment's author, anyone who may write the path, and anyone who may manage its grants may archive it.

A comment keeps pointing at its text. Its anchor is stored as the section's slug path. A section write that renames or re-ranks the section takes the comment with it; one that removes the section re-attaches the comment to the nearest surviving ancestor, else a sibling, else the top of the document, and marks it displacedFrom with the address it had. A moved file takes its comments along; a file created where a deleted one used to be starts with none.

Why sections matter#

Whole-file writes force an agent to hold the entire document in context and re-emit it perfectly. Section operations don't: an agent reads list_sections, pulls the one section it needs, and replaces exactly that. Smaller reads, smaller writes, no accidental rewrites of the 900 lines it didn't touch — and a cleaner diff in the revision history.

usesource://usesource/docs/sections.md