Files
oh-my-pi/docs/tools/edit.md
T
can1357 037bf15b53 feat(hashline): distinguished mismatch diagnostics for drifted vs unrecognized hashes
- Added a `hashRecognized` field to `MismatchDetails` and `MismatchError`, defaulting it to `true` for compatibility.
- Updated stale mismatch rejection messaging to distinguish drifted hashes from session-absent hashes with explicit guidance.
- Propagated `hashRecognized: snapshot !== null` in `Patcher` and added tests for both mismatch branches.
2026-05-29 06:42:47 +02:00

186 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# edit
> Applies source edits; default mode is the hashline patch language consumed from a single `input` string.
## Source
- Entry: `packages/coding-agent/src/edit/index.ts`
- Model-facing prompt: `packages/hashline/src/prompt.md`
- Key collaborators:
- `packages/coding-agent/src/utils/edit-mode.ts` — selects active edit mode
- `packages/hashline/src/grammar.lark` — hashline grammar
- `packages/hashline/src/format.ts` — sigils and header constants (`¶`, `#`, `@@`, `+`, `&`, `,`)
- `packages/hashline/src/input.ts` — parses `¶PATH#TAG` sections
- `packages/hashline/src/tokenizer.ts` / `packages/hashline/src/parser.ts` — tokenizes and parses ops
- `packages/hashline/src/apply.ts` — applies parsed edits to file text
- `packages/hashline/src/mismatch.ts` — stale-anchor mismatch formatting (distinguishes recognized-but-drifted from never-recorded hashes)
- `packages/hashline/src/recovery.ts` — snapshot-based stale-anchor recovery
- `packages/hashline/src/snapshots.ts` — mints and resolves per-path opaque snapshot tags
## Inputs
### Hashline mode (default)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | `string` | Yes | One or more file sections. Anchored sections start with `¶PATH#TAG`; hashless `¶PATH` is allowed only for new-file creation or BOF/EOF-only inserts. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
Patch language inside `input`:
- **File header**: `¶PATH#TAG` (or `¶PATH` for new-file / virtual-only hunks). `TAG` is three uppercase-hex chars minted by the session snapshot store.
- **Hunk header**: bare `A B` selects original lines A..B. Two numbers are REQUIRED — single-line ranges are written `A A` (`5 5`), not `5`. The range separator is normally whitespace; the parser also silently accepts `A-B`, `A..B`, and `A…B` (unicode ellipsis). Virtual variants `BOF` and `EOF` target positions before line 1 / after the last line.
- **Body rows** (one per line, immediately under the hunk header):
- `+TEXT` — add the literal line `TEXT` verbatim, including all leading whitespace.
- `+` alone — add one blank line.
- `&A..B` — re-emit original file lines A..B. Use this to keep some of the lines you selected. `&A` is accepted as `&A..A`.
- **Semantics**:
- The new content of the selected range is just the body rows top-to-bottom.
- **Empty body deletes the range entirely.**
- `BOF` / `EOF` with empty body is a no-op (nothing to insert).
Anchors come from `read`/`search` output. `read` emits a `¶PATH#TAG` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
### Tolerated input shapes (lenient parsing)
Because models reproduce nearby shapes (`read` output, `apply_patch` envelopes, unified-diff hunks), the parser is liberal about a handful of harmless variants:
- `A` (bare single number) — REJECTED. The parser throws `single-number hunk header "A" is no longer accepted`. Spell single-line ranges as `A A`.
- `A-B`, `A..B`, `A…B` — accepted as `A B` (any of hyphen, double-dot, or unicode ellipsis works as a silent separator).
- `&A` — accepted as `&A..A`.
- Bare body rows with no `+`/`&` prefix are auto-prepended with `+` and a `BARE_BODY_AUTO_PIPED_WARNING` is appended, BUT only when every row in that block is uniformly bare. Mixed `+`/raw blocks still throw.
- `+&A..B` rows (model mistakenly prefixed a repeat with `+`) are silently rerouted as `&A..B` repeats with `PLUS_PREFIXED_REPEAT_WARNING`.
- Identical-range hunks in the same patch are coalesced last-wins with `REPLACE_PAIR_COALESCED_WARNING`.
- An overlapping bare hunk followed by a concrete hunk is treated as a stale "before then after" pair; the bare hunk is dropped with `REPLACE_PAIR_COALESCED_OVERLAP_WARNING`.
- `*** Begin Patch` / `*** End Patch` envelopes are silently consumed. `*** Abort` terminates parsing silently — ops parsed before the marker still apply, no warning surfaced.
- `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:` apply_patch sentinels throw an `apply_patch sentinel … is not valid in hashline` error.
- `@@`-bracketed hunk headers (whether the apply_patch `@@ context @@` form or the unified-diff `@@ -N,M +N,M @@` shape) are rejected with an explicit "drop the `@@ ... @@` brackets" message — hashline hunks are bare `A B` lines.
## Outputs
- Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake.
- `content` contains one text block per call. For a successful single-file edit it is either:
- `<path>:` plus a compact diff preview from `packages/hashline/src/diff-preview.ts`, or
- `Updated <path>` / `Created <path>` when no compact preview text is emitted.
- Parse, apply, or recovery warnings are appended as:
```text
Warnings:
...
```
- `details` is `EditToolDetails` from `packages/coding-agent/src/edit/renderer.ts`:
- `diff`: unified diff string
- `firstChangedLine`: first changed post-edit line
- `diagnostics`: LSP/format result if available
- `op`: `"create"` or `"update"` for hashline mode
- `meta`: output metadata
- `perFileResults`: present for multi-section input
- Multi-section input returns one aggregated result with combined text and per-file details.
## Worked examples
Reference file (the exact shape `read` returns):
```text
¶a.ts#0A3
1:const X = "a";
2:const Y = X;
3:
4:console.log(X);
5:console.log(Y);
6:export { X, Y };
```
Replace line 1 with two lines:
```text
¶a.ts#0A3
1
+const X = "b";
+export const Y = X;
```
Insert BELOW line 5 (keep line 5, add after):
```text
¶a.ts#0A3
5
&5
+console.log(X + Y);
```
Insert ABOVE line 5 (add before, keep line 5):
```text
¶a.ts#0A3
5
+console.log(X + Y);
&5
```
Delete lines 4..5 entirely:
```text
¶a.ts#0A3
4 5
```
Insert at start and end of file:
```text
¶a.ts#0A3
BOF
+// header
EOF
+// trailer
```
Multi-file:
```text
¶src/a.ts#0A3
4
+const enabled = true;
¶src/b.ts#1F7
20
```
## Limits & Caps
- File snapshot tags are exactly three uppercase-hex chars minted by the per-session snapshot store.
- The visible mismatch report shows 2 lines of context on each side (`MISMATCH_CONTEXT`) in `packages/hashline/src/messages.ts`.
- Stale-anchor recovery uses `fuzzFactor: 0` in `packages/hashline/src/recovery.ts`.
- `HL_FILE_PREFIX` is `¶`, `HL_PAYLOAD_REPLACE` is `+`, `HL_PAYLOAD_REPEAT` is `&`, `HL_RANGE_SEP` is `..` (repeat-row bodies only), and `HL_FILE_HASH_SEP` is `#` (`packages/hashline/src/format.ts`). Hunk headers carry no sigil; the range is just two whitespace-separated line numbers.
## Errors
- Missing section header:
- `input must begin with "¶PATH#HASH" on the first non-blank line for anchored edits; got: ...`
- Missing tag for anchored edit:
- `Missing hashline snapshot tag for anchored edit to <path>; use ¶<path>#tag from your latest read/search output.`
- Stray payload line:
- `line N: payload line has no preceding hunk header. Use an \`A B\` (or \`BOF\` / \`EOF\`) line above the body. Got "...".`
- Raw body row with no `+` / `&` prefix in a mixed-prefix block:
- `line N: payload row in a hashline hunk must start with + or &A..B. Got "...".`
- Range out of order:
- `line N: range A..B ends before it starts.`
- Overlapping hunks on the same anchor:
- `line N: anchor line X is already targeted by another hunk on line Y. Issue ONE hunk per range; payload is only the final desired content, never a before/after pair.`
- apply_patch / unified-diff contamination:
- `line N: apply_patch sentinel "*** …" is not valid in hashline. File sections start with \`¶path#HASH\` (no \`Update File:\` / \`Add File:\` keyword). Hunks are bare \`A B\` lines with \`+TEXT\` / \`&A..B\` body rows.`
- `line N: unified-diff hunk header (\`@@ -N,M +N,M @@\`) is not valid in hashline. Hashline hunks are bare \`A B\` lines (or \`BOF\` / \`EOF\` keywords).`
- `line N: \`@@\`-bracketed hunk header "@@ …" is not valid in hashline. Drop the \`@@ ... @@\` brackets and write the range directly: \`5 7\` (\`BOF\` / \`EOF\` for virtual positions).`
- `line N: single-number hunk header "N" is no longer accepted. Spell single-line ranges as \`N N\` (two numbers); hashline hunks are bare \`A B\` lines (or \`BOF\` / \`EOF\`).`
- Out-of-range anchor:
- `Line N does not exist (file has M lines)`
- Stale snapshot tag: the `Patcher` first attempts snapshot-based recovery (3-way-merge of the model’s edits onto the current file via `packages/hashline/src/recovery.ts`, `fuzzFactor: 0`). When recovery cannot prove a valid result it throws `MismatchError`, which distinguishes two cases:
- **Hash recognized but file content drifted** (an in-session edit advanced the hash, or an external write changed the file): "file changed between read and edit" / "Section is bound to #X, but the current file hashes to #Y". Copy the post-edit hash from the prior edit response, or re-read.
- **Hash never recorded for this path** (likely fabricated or carried over from a prior session): "hash #X is not from this session". Re-read; never invent the tag.
In both cases the error includes the current file hash plus 2 lines of context around each anchor (`*LINE:TEXT` / ` LINE:TEXT`).
- No-op edit:
- `Edits to <path> parsed and applied cleanly, but produced no change: your body row(s) are byte-identical to the file at the targeted lines. The bug is somewhere else — re-read the file before issuing another edit. Do NOT widen the payload or add lines; verify the anchor first.`
- Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the mismatch error is surfaced unchanged.
## Warnings
- `Detected two identical-range hashline hunks; kept only the second hunk. …` (`REPLACE_PAIR_COALESCED_WARNING`)
- `Detected an overlapping bare hashline hunk immediately followed by a concrete hunk; dropped the earlier bare hunk. …` (`REPLACE_PAIR_COALESCED_OVERLAP_WARNING`)
- `Auto-prefixed bare body row(s) with +. Always start payload rows with +TEXT (literal) or &A..B (repeat) …` (`BARE_BODY_AUTO_PIPED_WARNING`)
- `A body row started with `+&A..B`. `+` (literal text) and `&A..B` (repeat) are sibling row kinds …` (`PLUS_PREFIXED_REPEAT_WARNING`)
- Recovery banners: `RECOVERY_EXTERNAL_WARNING`, `RECOVERY_SESSION_CHAIN_WARNING`, `RECOVERY_SESSION_REPLAY_WARNING` (`packages/hashline/src/messages.ts`).