<!-- Copyright 2026 The LumoSQL Authors, see LICENSES/CC-BY-SA-4.0 -->

<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
<!-- SPDX-FileCopyrightText: 2026 The LumoSQL Authors -->
<!-- SPDX-ArtifactOfProjectName: LumoSQL -->
<!-- SPDX-FileType: Documentation -->
<!-- SPDX-FileComment: Original by Dan Shearer, May 2026 -->

# Specification for fragment_patch behaviour

Not-forking no longer uses the system `patch(1)`, which has many implementations and no specification, and embody a great deal of ancient history and
workarounds. We replace all that with a small amount of code (<150 lines). The most important and often buggy features of `patch(1)` that we do *not* implement
are: fuzz, whitespace munging, and offset search. This specification documents the edge cases and decisions that `NotFork::Method::Fragment_patch::apply()`
still needs to make. The applier is deterministic. Its output depends only on the fragment bytes and the hunk bytes.

---

## 1. The fragment handling process

The anchor-based extraction in `apply()` stays as it is. For each fragment, the existing code splits the target file into three parts. `before` is the lines up
to the start-anchor match. `fragment` is the start-anchor line through the line before the end-anchor match. `after` is the end-anchor line onward.

This specification governs the middle step only, which transforms `fragment` in memory. The result is reassembled with `before` and `after` as it is now.

The applier takes `fragment` as an ordered list of lines, each keeping the trailing newline it has on disk (if any), plus the hunk body: the bytes between the
end-anchor line and the `---` terminator, which is the range `apply()` already reads. It returns the modified `fragment`.

---

## 2. Hunk parsing

The body holds one or more unified-diff hunks, each starting with a header line
matching `^@@ -a[,b] +c[,d] @@`.

**2.1 — the header numbers are advisory.** `a`, `b`, `c`, `d` never locate, size, or place anything. Diagnostics may quote them, and lint may check them, but
they do not drive application. If the header numbers are wrong it doesn't matter because the hunk will apply by content: keyinfo's `@@ -9,4 +9,7 @@` has a body
that is really 5 context and 3 additions, and we apply it anyway.

**2.2 — classify each body line by its first byte:**

| first byte | meaning | old-image | new-image |
|---|---|---|---|
| `' '` (space) | context | yes | yes |
| `'+'` | addition | — | yes |
| `'-'` | deletion | yes | — |
| line is exactly `\n` or empty | blank context | yes | yes |
| `'\'` (`\ No newline at end of file`) | newline directive | see §6 | see §6 |

**2.3 — marker stripping.** For a `' '`, `'+'`, or `'-'` line, drop one leading byte; what remains, trailing newline included, is the content. A line that is
exactly `\n` or empty counts as blank context with content `\n`. We accept both the `· \n` (space-marker) and bare `\n` forms of a blank context line, because
`fragment-diff` emits both. This is what lets us match the blank lines that `patch -F0` choked on in the rowsum case.

**2.4 — a malformed body** is a parse error: a non-empty line whose first byte is none of `' '`, `'+'`, `'-'`, `'\'`. Die naming the recipe, fragment, and the
offending line.

---

## 3. Old-image and new-image

For each hunk, in body order, the old-image is the content of every context and deletion line, and the new-image is the content of every context and addition
line. A hunk with no `+`/`-` lines is a no-op: its new-image equals its old-image, and it reproduces the input exactly.

---

## 4. Matching

**4.1 — exact contiguous match.** Find the old-image as a contiguous run of lines in `fragment`, comparing each line's content for byte equality, trailing
newline included (or its documented absence).

**4.2 — forward, monotonic window.** Hunks apply in body order. The search for hunk *k* starts at the fragment line right after hunk *k−1*'s matched run ended;
hunk 1 starts at index 0, and the window runs to the end of the fragment. Multi-hunk recipes stay order-deterministic, and a later hunk can never reach back
into earlier text.

**4.3 — die on ambiguity.** If the old-image matches more than once in the window, die. A hunk whose context is not unique inside its already-anchored fragment
is under-specified, and the cure is more context, not a guess. Anchors usually shrink the fragment to one function, so real ambiguity is rare; when it happens
we want to hear about it.

**4.4 — die on no match.** Zero matches means die. That is the right outcome for upstream drift: regenerate the recipe for this source version instead of
forcing it into place.

**4.5 — an empty old-image** (additions only) has nothing to anchor against, so die. `fragment-diff` always emits surrounding context, so this guards against a
hand-malformed recipe.

---

## 5. Splicing

Walk the hunks in order with a cursor `pos` into `fragment`, starting at 0. For each hunk: find the match position `at` (§4); emit `fragment[pos .. at-1]`
unchanged; emit the hunk's new-image; set `pos = at + length(old-image)`. After the last hunk, emit `fragment[pos .. end]`. Context lines are shared verbatim
between old- and new-image, so untouched regions and no-op hunks come out byte for byte.

---

## 6. Newlines and end of file

Lines normally carry a trailing `\n`, but the last line of `fragment` may not. A `\ No newline at end of file` directly after a context, addition, or deletion
line means that line has no trailing `\n`; at parse time we record the preceding line's content without it. Matching honours this, so a newline-less old line
matches only a newline-less source line. Emission honours it too, so a fragment whose last line lacks a newline round-trips unchanged, and an edit at
end-of-fragment neither adds nor drops a final newline unless the hunk says so. This case shows up at the end-anchor / EOF boundary and needs its own test.

---

## 7. What this buys

Output is a pure function of the fragment bytes and the hunk bytes, identical on every OS and in every `patch` or no-`patch` environment. `fragment_patch` stops
depending on `patch(1)`: the shared `patch_common` default goes back to `-tNsp1` for the plain `patch` method, and this method drops `patch` from its
prerequisites. A reviewer can read the whole applier — roughly 80 to 120 lines — and know what it will do, because there is no hidden heuristic. Drift and
under-specification fail loudly with the diagnostics below, where today they pass silently into a misplaced edit.

---

## 8. Errors and diagnostics

Messages speak in not-fork terms — recipe, fragment, anchor — never `patch`'s "Hunk #N". Each one names the recipe path and `.mod`, the target filename, the
start-anchor regex and its value if any, the end-anchor regex, and the `@@` header as the author wrote it.

A no-match also shows a closest near-miss: the source line-run with the smallest edit distance to the old-image, as a short expected-vs-found mini-diff. The
fuzzy compare builds this message and nothing else; it never affects whether or where a hunk applies. An ambiguity lists the fragment line numbers of the
competing matches. An empty old-image and a malformed body state their cause.

---

