Markdown patch 2.0#301
Draft
coddingtonbear wants to merge 25 commits into
Draft
Conversation
Introduce the 2.0 patch engine as a distinct package so the REST API can serve both engines side by side during the migration. Because both share the npm name `markdown-patch`, 2.0 is installed under the alias `markdown-patch-2`. During development it resolves to the local working copy via `file:../markdown-patch` (the two repos are siblings under Projects/); at release this will be swapped to a published npm alias, e.g. `"markdown-patch-2": "npm:markdown-patch@^2.0.0"`. `markdown-patch` continues to export the 1.x `applyPatch`/`getDocumentMap` surface; `markdown-patch-2` exports the 2.0 `patch()` engine. No call sites use the new package yet — this commit is only the plumbing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an opt-in second engine to the vault PATCH endpoint. When a request carries `MD-Patch-Version: 2`, the whole instruction is read from a JSON request body (an `InstructionInput` from markdown-patch 2.0) and applied by the 2.0 `patch()` engine; the URL supplies only the file. Without the header the request is served by the existing header-driven 1.x path, byte-for-byte as before, so no existing client is affected. The 2.0 algebra is far richer than the 1.x header vocabulary can express (four scopes including `parent`, a `destination` move carrier, arbitrary-JSON `value`, `ifMatch`), which is why v2 takes a structured JSON body rather than extending the header set. Details: - `VaultOperations.patchFileSectionV2` reads the file, applies the instruction, writes the result, and returns the `PatchResult` (document + warnings). - The handler validates the instruction discriminants (targetType/operation/ scope) up front for clean 400s, then maps the engine's typed errors: TargetNotFoundError -> 404, PreconditionFailedError (ifMatch) -> 412, ContentPreexistsError -> 409, InvalidCellError/other -> 400. - Advisory warnings (e.g. heading-depth overflow) are JSON-encoded in the `MD-Patch-Warnings` response header; the patched document is the body. - New `InvalidPatchInstruction` (40081) error code for malformed v2 bodies. - New v2 discriminant guards (isV2Operation/isV2Scope/isV2TargetType) alongside the 1.x guards, since 2.0 widens `operation` (adds delete) and `scope` (adds parent). Tests drive the real 2.0 engine end to end through the mock vault, covering the happy paths (content default, frontmatter `value`, warnings header), the error mappings, and that a request without the header still routes to 1.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the MD-Patch-Version header with a presence heuristic, matching how the markdown-patch-1.0 migration distinguished engines (by which headers a request carried, not a dedicated version header). Every valid 1.x PATCH requires a Target-Type header; a 2.0 request carries no Target-Type and puts its whole instruction in a JSON body. So `_vaultPatch` routes to the 2.0 engine when a request has no Target-Type header and an object body, and stays on the 1.x path otherwise — including a malformed 1.x request (no Target-Type, text body), which still gets the unchanged missing-Target-Type error. No new header surface, and 1.x behavior is preserved byte-for-byte. This applies to the active/ and periodic/ PATCH endpoints too, since both delegate to `_vaultPatch`. Also rename the handler and operation off "V2" (`_vaultPatchV2` -> `_vaultPatchMdp2`, `patchFileSectionV2` -> `patchFileSectionMdp2`): the removed deprecated API-version-2.0 PATCH handler was itself once named `_vaultPatchV2`, so "Mdp2" (markdown-patch 2.0) avoids overloading that meaning. Since routing now guarantees an object body, the handler drops its own non-object check and takes the parsed instruction directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the vault_patch MCP tool's 1.x schema and behavior with the markdown-patch 2.0 algebra. LLM callers don't need the backward-compatible 1.x format, so there is a single patch surface rather than two. The tool now accepts a structured instruction: operation (replace/prepend/ append/delete) applied to a scope (content/marker/markerAndContent/parent) of a target, with the payload in exactly one carrier chosen by what it is — `content` (a markdown/text string), `value` (arbitrary JSON, for frontmatter values), or `destination` (a heading move). Heading targets are addressed as an array of heading texts (matching the 2.0 REST JSON body), not a '::'-delimited string, and heading levels inside content are relative to the target. The handler assembles the instruction from whichever fields were supplied and delegates to VaultOperations.patchFileSectionMdp2; the engine validates the operation×scope×targetType combination and the carrier. Advisory warnings are returned alongside the OK result. The MCP tool schema must be a flat Zod object (the SDK's tool() signature), so the discriminated union is expressed as optional fields validated downstream rather than a z.discriminatedUnion. Also updates the tag-management guidance in the vault_tags tool description to the 2.0 `value` carrier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every response served by the header-driven 1.x PATCH path now carries `Deprecation: true; sunset-version="5.0"` (RFC 8594), announcing that the format is deprecated in favor of the 2.0 JSON instruction body and will be removed in 5.0. The header is set once the request is committed to the 1.x path, so it appears on both success and error responses, and it is absent from 2.0 responses. This mirrors how the previously-removed 2.x heading format was sunset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the PATCH endpoint as the markdown-patch 2.0 JSON-instruction format only, and reduce the deprecated 1.x header-driven format to an upgrade notice — matching how the previously-removed 2.x heading format was handled (left undocumented with a migration guide). - New `PatchInstruction` (and `HeadingAddress`) schema components describing the operation×scope×target algebra and the three payload carriers (content/value/destination). - `patch.jsonnet` now takes an `application/json` instruction body (no header parameters), with worked examples (heading append, table rows, frontmatter, tags, move). Documents the 409 (rejectIfContentPreexists) and 412 (ifMatch) responses and the `MD-Patch-Warnings` response header. - `descriptions/patch.md` rewritten around the JSON format, with a "Deprecated: the 1.x header-driven format" section carrying the header→field upgrade table and the `sunset-version="5.0"` notice. - MCP tools/call example uses the array heading target. - Regenerated docs/openapi.yaml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the quick-start and "Patching notes" examples to the JSON instruction body (targetType/target-array/operation/scope with content/value/destination carriers), note relative heading levels, the MD-Patch-Warnings header, and ifMatch. Add a deprecation callout for the header-driven 1.x format (removal in 5.0, Deprecation header) and clarify that PATCH no longer uses header targeting while GET/PUT/POST still do. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New src/integration/patch2.test.ts exercises the 2.0 JSON-instruction PATCH format against a live Obsidian instance: heading content (append/prepend/replace, default scope, nested array target, marker rename), block content, frontmatter via the `value` carrier (replace and list merge), delete, the MD-Patch-Warnings header on heading-depth overflow, ifMatch mismatch -> 412, createTargetIfMissing, target-not-found -> 404, invalid targetType -> 400, directory -> 405, and that 2.0 responses omit the Deprecation header while header-driven 1.x requests still work and carry it. Also migrates the vault_patch cases in mcp.test.ts to the 2.0 tool shape (array heading target, `value` carrier) and replaces the obsolete JSON-string-parsing case with a native-JSON-array case and an unresolvable-target error case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2.0 markdown-patch format becomes the default for PATCH, the document map, and targeted reads, with the 1.x behavior available only on explicit opt-in. That is a breaking change for existing REST clients, so the next release is a major bump. package.json is updated here; manifest.json/versions.json and the lockfile are regenerated by the release ceremony (npm run version) at ship time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the request-shape heuristic with an explicit Markdown-Patch-Version header. The 2.0 JSON-instruction format is now the default: an absent header or "2" routes to the 2.0 engine; "1" opts back into the deprecated header-driven format; any other value returns 400 (InvalidPatchVersionHeader, 40082). A 2.0 request with a non-object body returns 400 (InvalidPatchInstruction) with a hint to send application/json or set Markdown-Patch-Version: 1. This makes 2.0 the default for all clients (a breaking change for 1.x callers, hence the major bump) and moves selection off body shape so it can also govern the document map and targeted reads consistently. The 1.x Deprecation header now advertises sunset-version="6.0" — the major after this release. Existing 1.x unit tests opt in via the header; new tests cover the invalid header value, the non-object-body rejection, explicit "2", and header-forced 1.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The document map now returns the markdown-patch 2.0 PublicMap shape by default: heading addresses as null-padded arrays (pass one straight back as a patch or read target), bare block ids, frontmatter field names, and a content-hash `version` token clients send as a patch `ifMatch` precondition — closing the gap where 2.0's optimistic concurrency had no readable token. REST honors Markdown-Patch-Version: 1 to get the deprecated `::`-joined shape (with a Deprecation: sunset-version="6.0" header); an invalid header value is 400. The MCP vault_get_document_map tool is 2.0-only, matching the 2.0-only vault_patch tool, and its description now explains the array addresses and the version/ifMatch handshake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Targeted section reads (REST GET with Target-Type/Target headers, and the MCP vault_read tool) now resolve through the 2.0 model via the new readTarget helper. The extracted text is byte-identical to the 1.x extraction for every case, so read output is unchanged; what changes is the address grammar and the default. REST honors Markdown-Patch-Version: 1 to keep the deprecated 1.x extraction (with a Deprecation: sunset-version="6.0" header); the default 2.0 path splits the Target-Delimiter string into a heading path array internally, so the REST Target/Target-Delimiter header interface is unchanged. An invalid version header is 400. The MCP vault_read tool is 2.0-only: its heading target is now an array (a bare string is accepted and wrapped), matching vault_patch and vault_get_document_map; an array target for a block/frontmatter read is rejected. targetDelimiter is dropped from the tool since heading paths are arrays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coddingtonbear
marked this pull request as draft
July 19, 2026 23:04
Update the OpenAPI source, regenerated spec, and README for the 2.0-by-default switch: - Add a shared Markdown-Patch-Version header parameter (default 2; 1 opts into the deprecated format) to the GET and PATCH operations. - Rewrite the document-map response schema to the 2.0 shape: a version token, heading addresses as arrays, and bare block ids — noting the 1.x ::-joined shape is available via Markdown-Patch-Version: 1. - Move the patch deprecation notice off the old request-shape heuristic and onto the header; bump the advertised sunset from 5.0 to 6.0 everywhere. - Add the 400 InvalidPatchVersionHeader response and refresh the heading-depth guidance for array addresses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the live-Obsidian integration suite to the header-driven engine selection: - patch.test.ts opts every request into the deprecated format with Markdown-Patch-Version: 1 (that suite exercises 1.x on purpose). - patch2.test.ts: the embedded 1.x check now sends the opt-in header and expects sunset-version="6.0"; add cases for a 1.x-style request without the header falling through to the 2.0 engine (400) and an invalid version header (400). - vault.test.ts: the document-map test now asserts the 2.0 shape (array addresses + version) and adds a 1.x-map case; targeted reads gain default (no Deprecation) and 1.x (Deprecation: 6.0) coverage. - mcp.test.ts: vault_read nested target is an array; vault_get_document_map asserts the 2.0 array/version shape. - active.test.ts: the /active/ PATCH now uses the 2.0 JSON instruction body. Full suite green against a live Obsidian instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/plans/2026-06-11-001-fix-resolve-linter-warnings-plan.md was accidentally swept into 74fce0e alongside the Markdown-Patch-Version header work. It documents an unrelated ESLint cleanup effort and had the side effect of introducing a docs/plans/ directory that exists nowhere else in the repository's history. The plan is also still unimplemented on this branch: the require() imports in vaultOperations.ts, the unhandled .catch(next) promises in requestHandler.ts, and the .eslintrc parserOptions.project gap are all untouched. Removing it here rather than deleting the work outright -- the content remains recoverable from 74fce0e and can be reintroduced on a dedicated branch when the linter cleanup is picked up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_vault/ was accidentally swept into 74fce0e alongside the Markdown-Patch-Version header work. It is local scratch state, not a test fixture: nothing in the repository references the path, Welcome.md is the unmodified Obsidian scaffold, and the plugins/ entry is a symlink hardcoded to an absolute path under one developer's home directory that would not resolve for anyone else. It also carried per-user Obsidian UI state (workspace.json, graph.json, appearance.json) that churns on every launch and would generate noise in unrelated diffs. Added test_vault/ to .gitignore so a local vault at that path stays untracked going forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PATCH documentation repeatedly qualified itself as "the 2.0 format", "the 2.0 shape", or "the markdown-patch 2.0 algebra". That framing asks readers to hold a version distinction they have no reason to care about: for anyone meeting this API for the first time, the JSON instruction format simply is the API, and the qualifier reads as though there were a choice to make on every request. Dropped the version qualifiers from the primary descriptions across patch.md, targeting.md, the jsonnet sources, the PatchInstruction schema, the MCP tool description, the README, and the non-object-body error message, and regenerated docs/openapi.yaml. Retained version references in the two places they carry real information: the "Deprecated: the 1.x header-driven format" section, with the migration pointer near the top of patch.md for readers who know they are on the old format, and the Markdown-Patch-Version header's own documentation, where the 1-vs-2 distinction is the subject. That header is now described as something you do not normally need to send, rather than as a selector between two co-equal formats. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntent The PATCH description opened by telling readers there are no Operation/Target-* headers to set. For anyone meeting this endpoint for the first time that is noise: it introduces header names they have no reason to expect, purely to say they are absent. Removed the clause and trimmed the same enumeration from the README's deprecation aside, which now identifies the old format as "header-driven" and links to the interactive docs for the field-by-field mapping. PATCH's primary prose now names no legacy header at all; the mapping table in the deprecated section still spells each one out, where it is the subject. Left the Target-Type/Target documentation in targeting.md untouched. Those descriptions attach to GET/PUT/POST, where the headers remain the live targeting mechanism rather than deprecated surface. Also reworked the markerAndContent bullet, which described the scope as "the whole node/subtree" without saying the thing that actually distinguishes it from content: the heading line is inside the edited span, so replace rewrites the heading itself. Documented the level behavior confirmed against the engine -- content headings are rebased to the target's own level with internal nesting preserved -- and added the corresponding footgun: a markerAndContent replace whose content has no heading dissolves the section into a plain paragraph. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Blank lines are not synthesized" section gave advice that does not work, found by running it against the engine. It told callers to end content with \n\n for append/replace and start it with \n\n for prepend. A trailing \n\n on an append at the end of a document is normalized away entirely, and a leading \n\n on a prepend produces two blank lines rather than one. The correct separator in every case is a single leading newline, including for append. The framing was misleading too. Saying a blank line "is preserved only if one already existed" implies the API inspects the boundary, which invites the wrong prediction: prepending into a section whose heading is followed by a blank line still lands flush against the heading, because that blank line belongs to the body and is pushed below the inserted text. Replaced it with the rule the engine implements -- content is spliced verbatim at one edge of the target's span and the API contributes no whitespace of its own -- and worked prepend, append, and replace through one example, with and without the leading newline. Every quoted string is byte-exact against the engine and is pinned upstream by markdown-patch's docs.whitespace.test.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rename instructions made readers learn about heading markers and count `#` characters. For PATCH none of that is needed: a marker-scope replace takes the new text and preserves the level, so there is no depth to look up. Gave PATCH its own "Renaming a heading" section that says exactly that, plus the block-id and frontmatter-key equivalents. Including `#` characters is not merely unnecessary there, it is wrong. They are not stripped, so "## New Name" renames the heading to the literal `## New Name`. Since the deprecated header-driven format *required* those `#`s, anyone migrating will carry them over by habit and silently corrupt the heading, so both documents now warn about it explicitly. targeting.md needed more than trimming. It attaches to the GET/PUT/POST descriptions, which still run the 1.x engine where the `#`s are genuinely required -- but it illustrated the rule with a `curl -X PATCH` example, demonstrating the deprecated format under the endpoints it does not describe, and teaching PATCH readers the reverse of PATCH's actual rule. Switched the example to PUT, which is the verb that maps to a targeted replace, led with a pointer to PATCH as the easier path, and stated plainly that the two rules are opposites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rename guidance presented two methods and asked the reader to choose, which is a choice they have no reason to make: PATCH renames a heading from just its new text, and the header-driven alternative only adds `#`-counting and a depth lookup. Dropped the "prefer PATCH" framing along with the worked header example, the depth-inference explanation, and the "useful for renaming" aside on the marker bullet. targeting.md now points at PATCH in one line, and patch.md carries the whole explanation. The header-driven scope rules themselves are kept, condensed to a sentence. They still govern PUT and POST, which route through the 1.x engine, so removing them would leave that behavior undocumented rather than merely unmentioned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Condensing the rename guidance took out the side-by-side framing but left the note that closed it, which still opened "These two rules are exact opposites" with nothing to compare against. The preceding paragraph had the same problem in miniature, leading with "Here the marker scope..." where "here" had been contrasting with PATCH. Dropped the note and the dangling qualifier. The `#` warning it carried is not lost: patch.md states it at the point of use, where someone writing a rename instruction will actually meet it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The document map's `headings` moves from a flat list of null-padded path
arrays to a nested `HeadingTree` object: each heading's text maps to an
object of its child headings, and a leaf maps to `{}`. Nesting carries no
heading level, so a skipped source level leaves no hole, and a repeated
sibling appears once at its first occurrence in document order.
Adds the `HeadingTree` OpenAPI schema and points the map's `headings`
field at it, updates the MCP `vault_get_document_map`/`vault_read`/
`vault_patch` heading-address types (dropping the `null` skipped-level
element) and their descriptions, and updates unit and integration tests
to the nested shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… targeting
Targeted GET/PUT/POST previously ran the 1.x markdown-patch engine for both
URL path-element targeting (`.../heading/A/B`) and the `Target-Type`/`Target`
header form, carrying every 1.x quirk (manual `#` counting, heuristic
whitespace, `::` delimiter). Only PATCH had moved to the 2.0 engine.
This makes the version header the single switch:
- Default (2.0): a sub-part is addressed with URL path elements, routed through
the 2.0 engine, so heading levels are normalized and the engine owns boundary
whitespace. Headings are addressed array-natively from the URL segments, so a
heading containing `::` needs no escaping (resolvePathAndTarget now preserves
the segments as an array alongside the `::`-joined string).
- Markdown-Patch-Version: 1: the deprecated header-based targeting is processed
by the 1.x engine, and every 1.x response carries the
`Deprecation: true; sunset-version="6.0"` advisory (now on PUT/POST too).
- Header targeting without that header is rejected with the new
`400 HeaderTargetingRequiresVersion1` rather than silently operating on the
whole file — preventing a header-targeted write from clobbering the note.
`_vaultPatchTargeted` gains a `source` ("path" | "header") discriminant and a
shared `_respondMdp2` responder (extracted from the JSON-body PATCH path). The
same treatment is applied uniformly across the vault, active-file, and periodic
PUT/POST endpoints and the targeted GET read.
Docs stop documenting header-based targeting (removed from the GET/PUT/POST
parameter lists and rewritten around URL path elements in targeting.md and the
README); the header form survives only as a deprecation note. PUT/POST now
document the 2.0 `409` (Reject-If-Content-Preexists) and the MD-Patch-Warnings
header. Unit tests cover the new routing, gating, and level normalization;
integration tests are updated to the new behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The instruction algebra was written out three times: once as markdown-
patch-2's types, once as this project's MCP `vault_patch` tool input, and
once as the OpenAPI `PatchInstruction` component. The two copies here
were hand-maintained, drifted from the engine, and enforced none of its
cross-field rules -- the MCP `target` still allowed the null-padded array
elements the engine no longer uses, and the OpenAPI table-row example put
a `value` on a block target, which the 2.0 engine ignores entirely.
Derive both from markdown-patch-2's InstructionInputSchema instead:
- vault_patch spreads InstructionInputObjectSchema.shape for its
instruction fields, so the tool input is the schema's field set with
its descriptions, not a restatement.
- scripts/gen-patch-schema.mjs runs the same schema through
zod-to-json-schema to produce docs/src/lib/patchInstruction.schema.json,
which openapi.jsonnet imports as the PatchInstruction component.
build-docs runs the generator before jsonnet, and the generated file
is committed alongside openapi.yaml so the compiled spec is
reproducible. A small post-process rewrites how the OpenAPI-3 target
renders a null union branch (`enum: ["null"]`) into `nullable: true`.
- Replaced the stale block-table-row example with a block content
append, which is what the 2.0 engine actually does.
The engine now rejects a malformed instruction with a typed
InvalidInstructionError; map it to the same InvalidPatchInstruction
(40081) response as InvalidCellError, since both mean the caller sent an
instruction the algebra does not accept. Added unit and integration
coverage for the new cases: a write missing its carrier, a frontmatter
value write carrying `content`, and an operation×scope outside the
algebra.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.