blockpatch 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +18 -0
- package/README.md +470 -45
- package/dist/cli.js +2466 -246
- package/package.json +23 -11
- package/dist/cli.d.ts +0 -2
- package/dist/engine.d.ts +0 -19
- package/dist/errors.d.ts +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -263
- package/dist/parser.d.ts +0 -2
- package/dist/types.d.ts +0 -20
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
`blockpatch` is intentionally small. The core invariant is: no fuzz, no AST parsing, and no regeneration of moved bytes.
|
|
4
|
+
|
|
5
|
+
Changes should preserve these rules:
|
|
6
|
+
|
|
7
|
+
- context and payload matching stay byte-exact
|
|
8
|
+
- moved bytes come from the source file, not from formatted or regenerated text
|
|
9
|
+
- line numbers are review hints, not match authority
|
|
10
|
+
- agent-facing output stays deterministic and structured
|
|
11
|
+
|
|
12
|
+
Before sending changes, run:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun test
|
|
16
|
+
bun run build
|
|
17
|
+
npm publish --dry-run
|
|
18
|
+
```
|
package/README.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# blockpatch
|
|
2
2
|
|
|
3
|
-
`blockpatch`
|
|
3
|
+
`blockpatch` applies anchored text block relocation patches that look like unified diffs. It is not a generic diff tool, an AST refactor tool, a formatter, or a fuzzy matcher.
|
|
4
4
|
|
|
5
|
-
The core invariant is simple:
|
|
5
|
+
The core invariant is simple: a move transfers one exact, hash-verified payload between endpoints. In a paired move, the source hunk removes exact bytes and the target hunk adds the same exact bytes, so `blockpatch` moves the original source bytes instead of regenerating them.
|
|
6
|
+
|
|
7
|
+
A move may also be one-sided. A source-only hunk removes the verified payload from an existing file. A target-only hunk materializes the patch-carried payload into an existing file. File path absence is represented separately with `/dev/null` in file headers: `/dev/null -> file` creates a file, and `file -> /dev/null` removes a file.
|
|
8
|
+
|
|
9
|
+
`blockpatch` emits reviewable unified-diff-shaped patches that are intended to be compatible with `patch --fuzz=0` where possible. `blockpatch apply` accepts a strict, hash-verified subset of those patches and deliberately rejects fuzzy, heuristic, or ambiguous application.
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
14
|
npx blockpatch check patch.blockpatch
|
|
11
15
|
npx blockpatch apply patch.blockpatch
|
|
16
|
+
npx blockpatch plan --json -
|
|
17
|
+
npx blockpatch move --json -
|
|
12
18
|
bunx blockpatch apply patch.blockpatch --dry-run
|
|
13
19
|
npm install -g blockpatch
|
|
14
20
|
```
|
|
@@ -19,7 +25,8 @@ For local development:
|
|
|
19
25
|
bun install
|
|
20
26
|
bun test
|
|
21
27
|
bun run build
|
|
22
|
-
|
|
28
|
+
npm run publish:dry
|
|
29
|
+
node dist/cli.js version
|
|
23
30
|
```
|
|
24
31
|
|
|
25
32
|
## Commands
|
|
@@ -28,91 +35,509 @@ bun run publish:dry
|
|
|
28
35
|
blockpatch check patch.blockpatch
|
|
29
36
|
blockpatch apply patch.blockpatch
|
|
30
37
|
blockpatch apply patch.blockpatch --dry-run
|
|
38
|
+
blockpatch apply patch.blockpatch --reverse
|
|
39
|
+
blockpatch check patch.blockpatch -R
|
|
40
|
+
blockpatch apply - < patch.blockpatch
|
|
41
|
+
blockpatch apply < patch.blockpatch
|
|
42
|
+
blockpatch apply -i patch.blockpatch
|
|
43
|
+
blockpatch apply -d repo-root -p1 patch.blockpatch
|
|
44
|
+
blockpatch check -p1 < patch.blockpatch
|
|
45
|
+
blockpatch plan --json -
|
|
46
|
+
blockpatch move --json -
|
|
47
|
+
blockpatch move --json move.json
|
|
48
|
+
blockpatch move --src src/foo.ts --src-start $'\nfunction movedThing() {' --src-end $'\n}\n' --target-before $'class Target {\n'
|
|
49
|
+
blockpatch move --src /dev/null --dst src/foo.ts --payload $'inserted bytes\n' --target-before $'context before\n'
|
|
50
|
+
blockpatch move --src src/foo.ts --src-start $'\nfunction removeMe() {' --src-end $'\n}\n' --dst /dev/null
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
`check` parses the patch and verifies it against the target file without writing. `apply --dry-run` does the same validation through the apply path without writing.
|
|
53
|
+
`check` parses the patch and verifies it against the target file without writing. `apply --dry-run` does the same validation through the apply path without writing. `-R`/`--reverse` moves the verified payload back from the target location to the source location; it works with both `check` and `apply`.
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
Patch-declared source and destination paths, and move JSON `src`/`dst` paths, must be relative, non-empty, and resolve inside `--cwd`. Absolute operation paths, `..` escapes, and operation paths containing symlink components are rejected. Existing regular files are also realpath-checked; if the real path escapes `--cwd`, the operation is rejected. `-d`/`--directory` is an alias for `--cwd`. Patch files and move JSON files may be read from any path; use `--cwd` to choose the directory the operation is allowed to modify.
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
`apply` and `check` read the patch from stdin when no patch path is supplied. `-i`/`--input` names the patch file explicitly. `-pN`/`--strip N` strips leading path components from patch-declared file paths. Unlike GNU patch, `blockpatch` defaults to git-style `-p1` path stripping because patch headers require `a/` and `b/` prefixes.
|
|
58
|
+
|
|
59
|
+
`move` is the plug-and-play agent interface. JSON over stdin is the most reliable form because it avoids shell quoting problems:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
blockpatch move --json - <<'JSON'
|
|
63
|
+
{
|
|
64
|
+
"src": "src/foo.ts",
|
|
65
|
+
"src_start": "\nexport function movedThing(",
|
|
66
|
+
"src_end": "\n}\n",
|
|
67
|
+
"dst": "src/bar.ts",
|
|
68
|
+
"target_before": "export class Target {\n",
|
|
69
|
+
"target_after": "}\n"
|
|
42
70
|
}
|
|
71
|
+
JSON
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Insertion occurs between `target_before` and `target_after`. Either side may be omitted, but not both. With only `target_before`, the block is inserted after that context. With only `target_after`, the block is inserted before that context.
|
|
75
|
+
|
|
76
|
+
Matching and insertion are byte-exact: the moved bytes are cut at the source and inserted directly at the anchor boundary, with no newline handling. Keep delimiters and anchors on line boundaries or the result will splice mid-line. Include the surrounding newlines you want moved in `src_start` and `src_end`.
|
|
77
|
+
|
|
78
|
+
The move JSON and `--diff` planner interfaces are UTF-8 text interfaces. They are intended for source text, not arbitrary binary payloads or invalid UTF-8 byte sequences.
|
|
79
|
+
|
|
80
|
+
The same shape can be loaded from a file:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
blockpatch move --json move.blockpatch.json
|
|
84
|
+
```
|
|
43
85
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
Human-friendly flags are also supported:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
blockpatch move \
|
|
90
|
+
--src src/foo.ts \
|
|
91
|
+
--src-start $'\nexport function movedThing(' \
|
|
92
|
+
--src-end $'\n}\n' \
|
|
93
|
+
--dst src/bar.ts \
|
|
94
|
+
--target-before $'export class Target {\n' \
|
|
95
|
+
--target-after $'}\n' \
|
|
96
|
+
--expected-payload-sha256 <sha256>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Use `--dry-run` to validate without writing, `--diff` to print a reviewable `.blockpatch` document (`--diff` implies dry-run and never writes), and `--json-output` for machine-readable success or error output. Before `move --diff` returns success, it parses and checks its own emitted patch against the current tree in memory. `--explain` is a dry-run JSON alias for `--dry-run --json-output`; it reuses the existing `moves` byte ranges and payload hash fields.
|
|
100
|
+
|
|
101
|
+
For agents, the canonical planning handshake is:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
blockpatch plan --json -
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
That command is a thin alias for `blockpatch move --json - --diff --json-output`: it validates the provided source delimiters and/or target anchors, computes byte ranges, hashes the selected or supplied payload, renders the exact reviewable `.blockpatch`, self-checks that patch through the same in-memory `check` path, lists affected files, and returns the patch in the JSON `patch` field without mutating the working tree. The explicit `move --json - --diff --json-output` form remains supported.
|
|
108
|
+
|
|
109
|
+
`move --json --diff` is a planner for the current tree. For relocation and deletion, the JSON request selects the payload from the current source file; if that source block is already gone, the JSON request often cannot prove the final state because it does not carry the moved bytes. The generated `.blockpatch` is the retry/idempotence artifact because it carries the payload and can report `already_applied` from the final state. Target-only insertion JSON is the exception because it includes `payload` directly.
|
|
110
|
+
|
|
111
|
+
A typical flow is: propose a move as JSON, let `blockpatch` validate and render the exact patch, show that patch to the user, then apply the `.blockpatch` in a second explicit step. Retry the `.blockpatch`, not the source-selected JSON plan.
|
|
112
|
+
|
|
113
|
+
## Move JSON
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
type MoveBlockArgs = {
|
|
117
|
+
src: string
|
|
118
|
+
src_start?: string
|
|
119
|
+
src_end?: string
|
|
120
|
+
dst?: string
|
|
121
|
+
payload?: string
|
|
122
|
+
target_before?: string
|
|
123
|
+
target_after?: string
|
|
124
|
+
expected_payload_sha256?: string
|
|
125
|
+
dry_run?: boolean
|
|
47
126
|
}
|
|
127
|
+
```
|
|
48
128
|
|
|
49
|
-
|
|
50
|
-
|
|
129
|
+
Rules:
|
|
130
|
+
|
|
131
|
+
- In `move --json`, `/dev/null` denotes the absent source or target hunk endpoint for in-file insertion/deletion; `move --diff` renders those as normal same-file one-sided patch sections. Whole-file path creation/removal is expressed directly as `.blockpatch` documents with `/dev/null` file headers.
|
|
132
|
+
- For relocation, `src_start` and `src_end` are inclusive source delimiters; `dst` defaults to `src`.
|
|
133
|
+
- For deletion, set `dst` to `/dev/null`; `src_start` and `src_end` select the removed payload.
|
|
134
|
+
- For insertion, set `src` to `/dev/null`; `dst`, `payload`, and target context are required.
|
|
135
|
+
- `target_before`, `target_after`, or both are required for relocation and insertion.
|
|
136
|
+
- insertion is between the before and after contexts, and their concatenation must match exactly once.
|
|
137
|
+
- if only `target_before` is supplied, insertion is immediately after that context.
|
|
138
|
+
- if only `target_after` is supplied, insertion is immediately before that context.
|
|
139
|
+
- either target side may be empty when both are supplied, but not both may be empty.
|
|
140
|
+
- `payload` is only valid when `src` is `/dev/null`; it must be non-empty for in-file insertion.
|
|
141
|
+
- `expected_payload_sha256` is optional; in flag mode, pass `--expected-payload-sha256`; when supplied, the moved or materialized payload bytes must hash to that value before any write happens.
|
|
142
|
+
- source delimiter match must be unique.
|
|
143
|
+
- target anchor match must be unique.
|
|
144
|
+
- moved bytes are extracted from the source file, not regenerated from args.
|
|
145
|
+
- same-file source and target overlap is a hard failure.
|
|
146
|
+
|
|
147
|
+
Insertion:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"src": "/dev/null",
|
|
152
|
+
"dst": "src/foo.ts",
|
|
153
|
+
"payload": "inserted bytes\n",
|
|
154
|
+
"target_before": "context before\n"
|
|
51
155
|
}
|
|
156
|
+
```
|
|
52
157
|
|
|
53
|
-
|
|
54
|
-
|
|
158
|
+
Deletion:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"src": "src/foo.ts",
|
|
163
|
+
"src_start": "function removeMe() {\n",
|
|
164
|
+
"src_end": "}\n",
|
|
165
|
+
"dst": "/dev/null"
|
|
55
166
|
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## JSON Output
|
|
56
170
|
|
|
57
|
-
|
|
171
|
+
With `--json-output`, successful commands print:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
type ApplyResult = {
|
|
175
|
+
ok: true
|
|
176
|
+
changed: string[]
|
|
177
|
+
affected: string[]
|
|
178
|
+
written: boolean
|
|
179
|
+
noop: boolean
|
|
180
|
+
status: "applied" | "noop" | "already_applied"
|
|
181
|
+
strip_components?: number
|
|
182
|
+
patch?: string
|
|
183
|
+
moves: Array<{
|
|
184
|
+
id: string
|
|
185
|
+
src: string
|
|
186
|
+
dst: string
|
|
187
|
+
payload_sha256: string
|
|
188
|
+
payload_bytes: number
|
|
189
|
+
source_range: { start: number; end: number } | null
|
|
190
|
+
target_range: { start: number; end: number } | null
|
|
191
|
+
insert_index: number | null
|
|
192
|
+
}>
|
|
193
|
+
}
|
|
58
194
|
```
|
|
59
195
|
|
|
60
|
-
|
|
196
|
+
`changed` lists paths whose content changed, or would change during `check`, `--dry-run`, `--diff`, and `--explain`. `written` is true only when files were actually replaced by the command; it is false for `check`, `--dry-run`, `--diff`, `--explain`, `noop`, and `already_applied`. `affected` lists paths examined by the patch. `noop` is true when the patch validated but produced identical bytes. `status` is `applied` for a normal computed move, `noop` for a computed move whose output bytes are identical, and `already_applied` when the command can prove the requested final state is already present. `strip_components` is present for `check` and `apply` JSON success output and reports the effective `-p` path-stripping count; it defaults to `1`. `.blockpatch` apply/check can prove retry idempotence because the patch carries the payload; source-selected `move --json` relocation/deletion requests generally cannot after the source block is gone. `patch` is present when `move --diff --json-output` is used. In `already_applied` relocation results, `source_range` is `null` because the source block is no longer present. For target-only insertions, `source_range` is `null`. For source-only deletions, `target_range` and `insert_index` are `null`. For path creation/removal, `src` or `dst` is the string `/dev/null`. Human text output prints `changed <path>`, `would change <path>`, or `unchanged <path>`.
|
|
61
197
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
198
|
+
Errors print:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
type BlockPatchJsonError = {
|
|
202
|
+
ok: false
|
|
203
|
+
error: {
|
|
204
|
+
code: BlockPatchErrorCode
|
|
205
|
+
message: string
|
|
206
|
+
field?: string
|
|
207
|
+
path?: string
|
|
208
|
+
phase?: string
|
|
209
|
+
anchor?: string
|
|
210
|
+
matches?: number
|
|
211
|
+
ranges?: Array<{ start: number; end: number }>
|
|
212
|
+
line_ranges?: Array<{ start: number; end: number }>
|
|
213
|
+
}
|
|
65
214
|
}
|
|
66
215
|
```
|
|
67
216
|
|
|
68
|
-
|
|
217
|
+
Ambiguous-match errors include up to the first 10 exact byte ranges for the matched anchors or candidate source ranges, plus matching 1-based inclusive `line_ranges` when the relevant file bytes are available. They do not include source snippets, fuzzy suggestions, or repair guidance.
|
|
218
|
+
|
|
219
|
+
Error codes are the agent-facing branch contract. Removing a code or changing its meaning is semver-major.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
type BlockPatchErrorCode =
|
|
223
|
+
| "parse_error"
|
|
224
|
+
| "invalid_path"
|
|
225
|
+
| "path_outside_cwd"
|
|
226
|
+
| "symlink_path"
|
|
227
|
+
| "file_not_found"
|
|
228
|
+
| "not_regular_file"
|
|
229
|
+
| "permission_denied"
|
|
230
|
+
| "io_error"
|
|
231
|
+
| "source_not_found"
|
|
232
|
+
| "source_ambiguous"
|
|
233
|
+
| "target_not_found"
|
|
234
|
+
| "target_ambiguous"
|
|
235
|
+
| "destination_exists"
|
|
236
|
+
| "payload_mismatch"
|
|
237
|
+
| "hash_mismatch"
|
|
238
|
+
| "invalid_utf8"
|
|
239
|
+
| "target_overlaps_source"
|
|
240
|
+
| "already_applied"
|
|
241
|
+
| "invalid_move_args"
|
|
242
|
+
| "invalid_json"
|
|
243
|
+
| "missing_move_args"
|
|
244
|
+
| "unknown_command"
|
|
245
|
+
| "unknown_option"
|
|
246
|
+
| "invalid_option"
|
|
247
|
+
| "missing_option_value"
|
|
248
|
+
| "too_many_args"
|
|
249
|
+
| "unexpected_error"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`unexpected_error` is the generic fallback for non-`BlockPatchError` failures; agents should treat it as an internal failure and avoid branching on its message.
|
|
253
|
+
|
|
254
|
+
## V1 Format
|
|
255
|
+
|
|
256
|
+
```diff
|
|
257
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
258
|
+
blockpatch version 1
|
|
259
|
+
blockpatch move id=move-1 payload-sha256=bc8a95d6eb2b44aa564dbae1040ba8ff2273988ea43f0f3b0c47228f9dba6b3d
|
|
260
|
+
--- a/src/example.ts
|
|
261
|
+
+++ b/src/example.ts
|
|
262
|
+
|
|
263
|
+
@@ -1,8 +1,4 @@ blockpatch-source id=move-1 function movedThing
|
|
264
|
+
function alpha() {
|
|
265
|
+
}
|
|
266
|
+
-
|
|
267
|
+
-function movedThing() {
|
|
268
|
+
- console.log("keep me exact");
|
|
269
|
+
-}
|
|
270
|
+
function omega() {
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@@ -40,3 +36,7 @@ blockpatch-target id=move-1 constructor
|
|
274
|
+
constructor() {
|
|
275
|
+
}
|
|
276
|
+
+
|
|
277
|
+
+function movedThing() {
|
|
278
|
+
+ console.log("keep me exact");
|
|
279
|
+
+}
|
|
280
|
+
methodAfter() {
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Line numbers in hunk headers are review hints only. Application uses context and exact payload verification, not line numbers.
|
|
284
|
+
|
|
285
|
+
## One-Sided Hunks And Null Endpoints
|
|
286
|
+
|
|
287
|
+
Same-file sections may contain a source hunk and a target hunk, a source hunk only, or a target hunk only. One-sided hunks are for in-file insertion and deletion when the file exists both before and after the patch.
|
|
288
|
+
|
|
289
|
+
Target-only insertion into an existing file:
|
|
290
|
+
|
|
291
|
+
```diff
|
|
292
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
293
|
+
blockpatch version 1
|
|
294
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the added payload>
|
|
295
|
+
--- a/src/example.ts
|
|
296
|
+
+++ b/src/example.ts
|
|
297
|
+
|
|
298
|
+
@@ -1,2 +1,3 @@ blockpatch-target id=move-1
|
|
299
|
+
context before
|
|
300
|
+
+inserted line
|
|
301
|
+
context after
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Source-only deletion from an existing file:
|
|
69
305
|
|
|
70
|
-
|
|
306
|
+
```diff
|
|
307
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
308
|
+
blockpatch version 1
|
|
309
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the removed payload>
|
|
310
|
+
--- a/src/example.ts
|
|
311
|
+
+++ b/src/example.ts
|
|
71
312
|
|
|
72
|
-
|
|
313
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
314
|
+
context before
|
|
315
|
+
-doomed line
|
|
316
|
+
context after
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`/dev/null` is reserved for path absence. It appears bare, without an `a/` or `b/` prefix, exactly as in git diffs, so it can never collide with a real file named `dev/null` (which would appear as `a/dev/null`). The token is recognized during parsing and is never resolved, opened, or checked against `--cwd`; path validation and `-p` stripping do not apply to it.
|
|
320
|
+
|
|
321
|
+
Use `/dev/null` only when the file path itself is absent before or after the patch. A move from `/dev/null` creates a file from a whole-file target payload:
|
|
322
|
+
|
|
323
|
+
```diff
|
|
324
|
+
diff --blockpatch /dev/null b/src/new.txt
|
|
325
|
+
blockpatch version 1
|
|
326
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the file payload>
|
|
327
|
+
--- /dev/null
|
|
328
|
+
+++ b/src/new.txt
|
|
329
|
+
|
|
330
|
+
@@ -0,0 +1,2 @@ blockpatch-target id=move-1
|
|
331
|
+
+first line
|
|
332
|
+
+second line
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
A move to `/dev/null` removes a file after verifying a whole-file source payload:
|
|
336
|
+
|
|
337
|
+
```diff
|
|
338
|
+
diff --blockpatch a/src/old.txt /dev/null
|
|
339
|
+
blockpatch version 1
|
|
340
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the file payload>
|
|
341
|
+
--- a/src/old.txt
|
|
342
|
+
+++ /dev/null
|
|
343
|
+
|
|
344
|
+
@@ -1,2 +0,0 @@ blockpatch-source id=move-1
|
|
345
|
+
-first line
|
|
346
|
+
-second line
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The section shapes and their meaning:
|
|
350
|
+
|
|
351
|
+
| Shape | Meaning |
|
|
352
|
+
| --- | --- |
|
|
353
|
+
| `file -> file`, source + target hunks | relocation |
|
|
354
|
+
| `file -> file`, target hunk only | insert payload into an existing file |
|
|
355
|
+
| `file -> file`, source hunk only | delete payload from an existing file |
|
|
356
|
+
| `/dev/null -> file`, target hunk only | create a file |
|
|
357
|
+
| `file -> /dev/null`, source hunk only | remove a file |
|
|
358
|
+
| `/dev/null -> /dev/null` | invalid |
|
|
359
|
+
|
|
360
|
+
The key distinction is that an empty file is a real endpoint while `/dev/null` is the null endpoint. A missing file is an error unless the patch explicitly says `/dev/null`; missing files never silently resolve as empty files.
|
|
361
|
+
|
|
362
|
+
Rules for target-only insertion into an existing file:
|
|
363
|
+
|
|
364
|
+
- the section contains exactly one `blockpatch-target` hunk and no source hunk.
|
|
365
|
+
- the payload comes from the `+` lines and must match `payload-sha256`.
|
|
366
|
+
- the file must exist; missing files are not treated as empty files.
|
|
367
|
+
- `target context before + target context after` must match exactly once; the payload is inserted at the boundary.
|
|
368
|
+
- at least one side of target context is required, so arbitrary zero-byte in-file insertions are not valid.
|
|
369
|
+
- if `target before + payload + target after` is already present exactly once, the result is `already_applied`.
|
|
370
|
+
|
|
371
|
+
Rules for source-only deletion from an existing file:
|
|
372
|
+
|
|
373
|
+
- the section contains exactly one `blockpatch-source` hunk and no target hunk.
|
|
374
|
+
- the file must exist; missing files are not treated as empty files.
|
|
375
|
+
- `source context before + payload + source context after` must match exactly once; the payload bytes are removed.
|
|
376
|
+
- if removal leaves zero bytes, the file remains as an empty file.
|
|
377
|
+
- retries are idempotent: adjacent source anchors, or an absent anchorless payload, report `already_applied`.
|
|
378
|
+
|
|
379
|
+
Rules for `/dev/null -> file` creation:
|
|
380
|
+
|
|
381
|
+
- the section contains exactly one `blockpatch-target` hunk and no source hunk.
|
|
382
|
+
- the target hunk must be whole-file payload: only contiguous `+` payload lines, with no context lines.
|
|
383
|
+
- zero-byte payload is valid and creates an empty file.
|
|
384
|
+
- a missing destination is created, including parent directories; new files are created with mode 0644.
|
|
385
|
+
- if the destination already exists with exactly the requested bytes, the result is `already_applied`; if it exists with different bytes, the patch fails.
|
|
386
|
+
|
|
387
|
+
Rules for `file -> /dev/null` removal:
|
|
388
|
+
|
|
389
|
+
- the section contains exactly one `blockpatch-source` hunk and no target hunk.
|
|
390
|
+
- the source hunk must be whole-file payload: only contiguous `-` payload lines, with no context lines.
|
|
391
|
+
- zero-byte payload is valid and removes an empty file.
|
|
392
|
+
- the existing file bytes must exactly equal the source payload; then the path is removed.
|
|
393
|
+
- if the source path is already missing, the result is `already_applied`.
|
|
394
|
+
|
|
395
|
+
`-R`/`--reverse` swaps source and target hunk roles: reversing a target-only insertion deletes the inserted payload, reversing a source-only deletion re-inserts the payload, reversing a file creation removes the created file, and reversing a file removal recreates it.
|
|
396
|
+
|
|
397
|
+
In JSON output, a target-only insertion has `source_range: null`, and a source-only deletion has `target_range: null` and `insert_index: null`. A null path endpoint is rendered as the string `/dev/null` in `src` or `dst`.
|
|
398
|
+
|
|
399
|
+
Same-file moves use one file section. In that shape, the `--- a/<path>` and `+++ b/<path>` headers must name the same file after normal path cleanup.
|
|
400
|
+
|
|
401
|
+
Cross-file moves use two conventional file sections tied by the same move id and payload hash: one `role=source` section for the source file and one `role=target` section for the target file. Each section's `---` and `+++` headers name the same file. This avoids the misleading patch shape where `--- a/source.ts` and `+++ b/target.ts` look like a transformation from one filename into another.
|
|
402
|
+
|
|
403
|
+
The `a/` and `b/` prefixes are required, and each `diff --blockpatch` line must name the same two raw paths as that section's file headers. Unlike GNU patch, `blockpatch` defaults to git-style `-p1` path stripping: `a/src/file.ts` and `b/src/file.ts` resolve as `src/file.ts`; use `-p0` only if your working tree contains literal `a/` and `b/` directories.
|
|
404
|
+
|
|
405
|
+
## Grammar
|
|
406
|
+
|
|
407
|
+
Same-file move, insertion, or deletion:
|
|
73
408
|
|
|
74
|
-
|
|
409
|
+
```text
|
|
410
|
+
diff --blockpatch a/<path> b/<path>
|
|
411
|
+
blockpatch version 1
|
|
412
|
+
blockpatch move id=<id> payload-sha256=<sha256>
|
|
413
|
+
--- a/<path>
|
|
414
|
+
+++ b/<path>
|
|
415
|
+
|
|
416
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-source id=<id> optional label
|
|
417
|
+
[ <source context before> ]
|
|
418
|
+
-<moved payload>
|
|
419
|
+
[ <source context after> ]
|
|
420
|
+
|
|
421
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-target id=<id> optional label
|
|
422
|
+
[ <target context before> ]
|
|
423
|
+
+<same moved payload>
|
|
424
|
+
[ <target context after> ]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
For relocation, include both hunks and the target `+` payload must equal the source `-` payload. For source-only deletion, omit the target hunk. For target-only insertion, omit the source hunk and the payload comes from the target `+` lines.
|
|
428
|
+
|
|
429
|
+
Cross-file move:
|
|
430
|
+
|
|
431
|
+
```text
|
|
432
|
+
diff --blockpatch a/<source-path> b/<source-path>
|
|
433
|
+
blockpatch version 1
|
|
434
|
+
blockpatch move id=<id> role=source payload-sha256=<sha256>
|
|
435
|
+
--- a/<source-path>
|
|
436
|
+
+++ b/<source-path>
|
|
437
|
+
|
|
438
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-source id=<id> optional label
|
|
439
|
+
[ <source context before> ]
|
|
440
|
+
-<moved payload>
|
|
441
|
+
[ <source context after> ]
|
|
442
|
+
|
|
443
|
+
diff --blockpatch a/<target-path> b/<target-path>
|
|
444
|
+
blockpatch version 1
|
|
445
|
+
blockpatch move id=<id> role=target payload-sha256=<sha256>
|
|
446
|
+
--- a/<target-path>
|
|
447
|
+
+++ b/<target-path>
|
|
448
|
+
|
|
449
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-target id=<id> optional label
|
|
450
|
+
[ <target context before> ]
|
|
451
|
+
+<same moved payload>
|
|
452
|
+
[ <target context after> ]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Source context before and after are exact byte anchors. Either side may be empty, and payload-only source hunks are allowed if the payload is unique. Target hunks for existing files must include context on at least one side.
|
|
456
|
+
|
|
457
|
+
`blockpatch move` metadata keys must be unique. The recognized keys are `id`, `payload-sha256`, and `role`; unknown keys are rejected unless they use the reserved `x-` extension prefix.
|
|
75
458
|
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
459
|
+
The `-<old-start>,<old-count> +<new-start>,<new-count>` ranges are line-number hints for review, not match authority. `blockpatch` validates the line counts against the hunk body, but it locates changes by exact context and payload bytes.
|
|
460
|
+
|
|
461
|
+
For target hunks in existing files, `blockpatch` matches `target context before + target context after` exactly once in the destination file and inserts at `start + target context before.length`.
|
|
462
|
+
|
|
463
|
+
That means insertion occurs between target-before and target-after context:
|
|
464
|
+
|
|
465
|
+
```diff
|
|
466
|
+
@@ -40,2 +40,3 @@ blockpatch-target id=move-1
|
|
467
|
+
context before
|
|
468
|
+
+moved payload
|
|
469
|
+
context after
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Either target side may be empty, but not both, unless the target hunk is a whole-file `/dev/null -> file` creation hunk.
|
|
79
473
|
|
|
80
474
|
## Semantics
|
|
81
475
|
|
|
82
476
|
For one patch:
|
|
83
477
|
|
|
84
|
-
1. Parse
|
|
85
|
-
2.
|
|
86
|
-
3.
|
|
87
|
-
4.
|
|
88
|
-
5.
|
|
89
|
-
6.
|
|
90
|
-
7.
|
|
91
|
-
8.
|
|
92
|
-
9.
|
|
478
|
+
1. Parse one same-file section with source+target, source-only, or target-only hunks; or parse two cross-file relocation sections tied by `role=source` and `role=target`.
|
|
479
|
+
2. Verify hunk ids match `blockpatch move id=<id>`.
|
|
480
|
+
3. Extract source payload from contiguous `-` lines when a source hunk exists.
|
|
481
|
+
4. Extract target payload from contiguous `+` lines when a target hunk exists.
|
|
482
|
+
5. For paired moves, verify target payload exactly equals source payload.
|
|
483
|
+
6. Verify `payload-sha256` matches the exact moved or materialized payload bytes.
|
|
484
|
+
7. For source hunks, locate exactly one source match for `source context before + payload + source context after`.
|
|
485
|
+
8. For target hunks in existing files, locate exactly one target match for `target context before + target context after`.
|
|
486
|
+
9. Fail if a same-file target context range overlaps the source payload bytes.
|
|
487
|
+
10. Apply the hunk transition: remove source payload, insert target payload, or both.
|
|
488
|
+
11. Apply any path-state transition from `/dev/null`: create the missing destination path or remove the source path.
|
|
489
|
+
12. Write changed files with temp-file-and-rename replacement.
|
|
490
|
+
|
|
491
|
+
If the requested final state is already present, `blockpatch` reports `already_applied`. For paired moves, that means the source full match is absent and `target context before + payload + target context after` is present exactly once. For target-only insertion, the target payload is already between the target anchors. For source-only deletion, the source anchors are already adjacent or an anchorless payload is absent. This is strict idempotence for retries; it does not search fuzzily or infer moved bytes.
|
|
492
|
+
|
|
493
|
+
With `-R`/`--reverse`, `blockpatch` swaps hunk roles and path endpoints. Reverse application is exact and non-fuzzy. A payload-only source hunk has no source-side anchor for reverse insertion, so reverse requires source context before or after unless it is a whole-file path recreation.
|
|
494
|
+
|
|
495
|
+
Same-file moves are atomic at file-replacement granularity. Cross-file moves preflight both files and stage all changed temp files before renaming any original. If staging fails, originals are left untouched. Once renames begin, the two-file operation is still not transactional; the destination is renamed before the source so an interruption can duplicate the payload, but should not delete it from both files. Atomic here means per-file replacement, not a crash-durable multi-file transaction.
|
|
93
496
|
|
|
94
497
|
## Failure Rules
|
|
95
498
|
|
|
96
|
-
`blockpatch` exits non-zero and does not modify
|
|
499
|
+
`blockpatch` exits non-zero and does not modify files when:
|
|
97
500
|
|
|
98
501
|
- the patch file is malformed
|
|
502
|
+
- both endpoints are `/dev/null`
|
|
503
|
+
- a patch-declared or move-declared source/destination path is absolute, invalid, or escapes `--cwd`
|
|
504
|
+
- a referenced file is missing (and the patch does not say `/dev/null` where creation or removal would make that legal), not a regular file, unreadable, unwritable, or otherwise hits a filesystem error
|
|
505
|
+
- a target-only existing-file insertion has no target context
|
|
506
|
+
- a `/dev/null -> file` creation targets an existing file with different bytes
|
|
507
|
+
- a `file -> /dev/null` removal hunk does not match the whole file
|
|
99
508
|
- source anchors are missing
|
|
100
509
|
- source anchors or full source are ambiguous
|
|
101
|
-
- the located source payload does not exactly match
|
|
510
|
+
- the located source payload does not exactly match the source hunk payload
|
|
511
|
+
- target added payload does not exactly match source removed payload
|
|
512
|
+
- `payload-sha256` does not match the moved payload
|
|
513
|
+
- `move --diff` would need to render invalid UTF-8 bytes
|
|
102
514
|
- the target anchor is missing
|
|
103
515
|
- the target anchor is ambiguous
|
|
104
516
|
- the target anchor overlaps the source payload
|
|
105
|
-
- file I/O fails before
|
|
517
|
+
- file I/O fails before a write completes
|
|
518
|
+
|
|
519
|
+
## Byte Rules
|
|
520
|
+
|
|
521
|
+
Hunk body lines use unified-diff prefixes:
|
|
522
|
+
|
|
523
|
+
- space for context
|
|
524
|
+
- `-` for source payload
|
|
525
|
+
- `+` for target payload
|
|
526
|
+
|
|
527
|
+
The byte content after the prefix is matched exactly, including line endings. The standard `` marker is supported for a hunk body line without a trailing newline.
|
|
528
|
+
|
|
529
|
+
`.blockpatch` `apply` and `check` preserve parsed hunk body bytes exactly, including CRLF and no-trailing-newline cases. `move --json` and generated `move --diff` output render anchors and payloads as UTF-8 text, so they are not a binary-safe round trip for invalid UTF-8.
|
|
530
|
+
|
|
531
|
+
Blank lines are separators between the header and hunks. A blank line inside a hunk body is an error; encode an empty context line as a single space.
|
|
106
532
|
|
|
107
533
|
## Intentionally Out Of Scope
|
|
108
534
|
|
|
109
|
-
|
|
535
|
+
The current format does not implement:
|
|
110
536
|
|
|
537
|
+
- multiple independent moves in one patch document
|
|
538
|
+
- arbitrary generated diffs from before/after file snapshots
|
|
111
539
|
- fuzzy matching
|
|
112
540
|
- AST parsing
|
|
113
541
|
- code formatting
|
|
114
|
-
- multi-file patches
|
|
115
542
|
- copy operations
|
|
116
|
-
- generated diffs
|
|
117
|
-
- line numbers
|
|
118
543
|
- regex anchors
|