blockpatch 1.0.0 → 1.1.1
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 +9 -1
- package/README.md +100 -468
- package/conformance/README.md +29 -0
- package/conformance/cases/ambiguous-source/case.json +8 -0
- package/conformance/cases/ambiguous-target/case.json +8 -0
- package/conformance/cases/create-file/case.json +8 -0
- package/conformance/cases/crlf/case.json +10 -0
- package/conformance/cases/cross-file-relocation/case.json +12 -0
- package/conformance/cases/delete-existing-file/case.json +10 -0
- package/conformance/cases/insert-existing-file/case.json +10 -0
- package/conformance/cases/no-trailing-newline/case.json +10 -0
- package/conformance/cases/remove-file/case.json +9 -0
- package/conformance/cases/same-file-relocation/case.json +10 -0
- package/conformance/runner.mjs +221 -0
- package/dist/cli.js +1038 -526
- package/docs/behavior.md +207 -0
- package/docs/commands.md +88 -0
- package/docs/spec.md +366 -0
- package/examples/README.md +15 -0
- package/examples/create-file/README.md +9 -0
- package/examples/create-file/expected/file.txt +1 -0
- package/examples/create-file/patch.blockpatch +8 -0
- package/examples/create-file/work/.gitkeep +1 -0
- package/examples/cross-file-relocation/README.md +10 -0
- package/examples/cross-file-relocation/expected/source.txt +2 -0
- package/examples/cross-file-relocation/expected/target.txt +6 -0
- package/examples/cross-file-relocation/patch.blockpatch +26 -0
- package/examples/cross-file-relocation/work/source.txt +5 -0
- package/examples/cross-file-relocation/work/target.txt +3 -0
- package/examples/delete-existing-file/README.md +9 -0
- package/examples/delete-existing-file/expected/file.txt +2 -0
- package/examples/delete-existing-file/patch.blockpatch +10 -0
- package/examples/delete-existing-file/work/file.txt +3 -0
- package/examples/failure-ambiguous-target/README.md +9 -0
- package/examples/failure-ambiguous-target/patch.blockpatch +14 -0
- package/examples/failure-ambiguous-target/work/file.txt +5 -0
- package/examples/insert-existing-file/README.md +9 -0
- package/examples/insert-existing-file/expected/file.txt +3 -0
- package/examples/insert-existing-file/patch.blockpatch +10 -0
- package/examples/insert-existing-file/work/file.txt +2 -0
- package/examples/remove-file/README.md +9 -0
- package/examples/remove-file/expected/.gitkeep +1 -0
- package/examples/remove-file/patch.blockpatch +8 -0
- package/examples/remove-file/work/file.txt +1 -0
- package/examples/reverse/README.md +9 -0
- package/examples/reverse/expected/file.txt +4 -0
- package/examples/reverse/patch.blockpatch +14 -0
- package/examples/reverse/work/file.txt +4 -0
- package/examples/same-file-relocation/README.md +9 -0
- package/examples/same-file-relocation/expected/file.txt +4 -0
- package/examples/same-file-relocation/patch.blockpatch +14 -0
- package/examples/same-file-relocation/work/file.txt +4 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -1,543 +1,175 @@
|
|
|
1
1
|
# blockpatch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/cheese-melted/blockpatch/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/blockpatch)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Cut/paste for agents: hash-verified block moves that read like unified diffs.
|
|
6
7
|
|
|
7
|
-
|
|
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.
|
|
8
|
+
`blockpatch` emits reviewable unified-diff-shaped patches for exact, hash-verified payload moves. 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. Generated patches are intended to be compatible with `patch --fuzz=0` where possible, while `blockpatch apply` accepts a strict subset and deliberately rejects fuzzy, heuristic, or ambiguous application.
|
|
10
9
|
|
|
11
10
|
## Install
|
|
12
11
|
|
|
13
12
|
```sh
|
|
14
|
-
npx blockpatch check patch.blockpatch
|
|
15
|
-
npx blockpatch apply patch.blockpatch
|
|
16
|
-
npx blockpatch plan --json -
|
|
17
|
-
npx blockpatch move --json -
|
|
18
|
-
bunx blockpatch apply patch.blockpatch --dry-run
|
|
19
13
|
npm install -g blockpatch
|
|
14
|
+
blockpatch --help
|
|
20
15
|
```
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
Or run without installing:
|
|
23
18
|
|
|
24
19
|
```sh
|
|
25
|
-
|
|
26
|
-
bun test
|
|
27
|
-
bun run build
|
|
28
|
-
npm run publish:dry
|
|
29
|
-
node dist/cli.js version
|
|
20
|
+
npx blockpatch --help
|
|
30
21
|
```
|
|
31
22
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
blockpatch check patch.blockpatch
|
|
36
|
-
blockpatch apply patch.blockpatch
|
|
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
|
|
51
|
-
```
|
|
23
|
+
## Usage
|
|
52
24
|
|
|
53
|
-
`
|
|
25
|
+
`blockpatch` is a deterministic move planner/apply layer for coding agents:
|
|
54
26
|
|
|
55
|
-
|
|
27
|
+
1. Send a JSON move request to `blockpatch move --json - --diff`.
|
|
28
|
+
2. Show the emitted `.blockpatch` to the user for review.
|
|
29
|
+
3. Apply the reviewed patch with `blockpatch apply`.
|
|
30
|
+
4. Retry the `.blockpatch`, not the original JSON request.
|
|
56
31
|
|
|
57
|
-
|
|
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"
|
|
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
|
-
```
|
|
85
|
-
|
|
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
|
|
32
|
+
Example: move `movedThing` from `src/foo.ts` into `src/bar.ts`, starting from this tree:
|
|
114
33
|
|
|
115
34
|
```ts
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
35
|
+
// before: src/foo.ts
|
|
36
|
+
export function keepThing() {
|
|
37
|
+
return 7;
|
|
126
38
|
}
|
|
127
|
-
```
|
|
128
39
|
|
|
129
|
-
|
|
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"
|
|
40
|
+
export function movedThing() {
|
|
41
|
+
return 42;
|
|
155
42
|
}
|
|
43
|
+
|
|
44
|
+
// before: src/bar.ts
|
|
45
|
+
export const target = "here";
|
|
156
46
|
```
|
|
157
47
|
|
|
158
|
-
|
|
48
|
+
The JSON request selects the source bytes and target anchors. Ask `blockpatch` to plan the byte-exact move and emit the reviewable patch:
|
|
159
49
|
|
|
160
|
-
```
|
|
50
|
+
```sh
|
|
51
|
+
blockpatch move --json - --diff <<'JSON' > patch.blockpatch
|
|
161
52
|
{
|
|
162
53
|
"src": "src/foo.ts",
|
|
163
|
-
"src_start": "function
|
|
54
|
+
"src_start": "\nexport function movedThing() {\n",
|
|
164
55
|
"src_end": "}\n",
|
|
165
|
-
"dst": "/
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## JSON Output
|
|
170
|
-
|
|
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
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
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>`.
|
|
197
|
-
|
|
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
|
-
}
|
|
56
|
+
"dst": "src/bar.ts",
|
|
57
|
+
"target_before": "export const target = \"here\";\n"
|
|
214
58
|
}
|
|
59
|
+
JSON
|
|
215
60
|
```
|
|
216
61
|
|
|
217
|
-
|
|
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
|
|
62
|
+
`patch.blockpatch` now carries the move as a unified-diff-shaped document:
|
|
255
63
|
|
|
256
64
|
```diff
|
|
257
|
-
diff --blockpatch a/src/
|
|
65
|
+
diff --blockpatch a/src/foo.ts b/src/foo.ts
|
|
258
66
|
blockpatch version 1
|
|
259
|
-
blockpatch move id=move-1 payload-sha256=
|
|
260
|
-
--- a/src/
|
|
261
|
-
+++ b/src/
|
|
67
|
+
blockpatch move id=move-1 role=source payload-sha256=bb03c42613e9289c043d2fced7ce2d8c87410cdb15fa48341ce79fa409d45303
|
|
68
|
+
--- a/src/foo.ts
|
|
69
|
+
+++ b/src/foo.ts
|
|
262
70
|
|
|
263
|
-
@@ -
|
|
264
|
-
function alpha() {
|
|
71
|
+
@@ -3,5 +3,1 @@ blockpatch-source id=move-1
|
|
265
72
|
}
|
|
266
73
|
-
|
|
267
|
-
-function movedThing() {
|
|
268
|
-
-
|
|
74
|
+
-export function movedThing() {
|
|
75
|
+
- return 42;
|
|
269
76
|
-}
|
|
270
|
-
function omega() {
|
|
271
|
-
}
|
|
272
77
|
|
|
273
|
-
|
|
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
|
|
78
|
+
diff --blockpatch a/src/bar.ts b/src/bar.ts
|
|
293
79
|
blockpatch version 1
|
|
294
|
-
blockpatch move id=move-1 payload-sha256
|
|
295
|
-
--- a/src/
|
|
296
|
-
+++ b/src/
|
|
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:
|
|
80
|
+
blockpatch move id=move-1 role=target payload-sha256=bb03c42613e9289c043d2fced7ce2d8c87410cdb15fa48341ce79fa409d45303
|
|
81
|
+
--- a/src/bar.ts
|
|
82
|
+
+++ b/src/bar.ts
|
|
305
83
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
314
|
-
context before
|
|
315
|
-
-doomed line
|
|
316
|
-
context after
|
|
84
|
+
@@ -1,1 +1,5 @@ blockpatch-target id=move-1
|
|
85
|
+
export const target = "here";
|
|
86
|
+
+
|
|
87
|
+
+export function movedThing() {
|
|
88
|
+
+ return 42;
|
|
89
|
+
+}
|
|
317
90
|
```
|
|
318
91
|
|
|
319
|
-
|
|
92
|
+
`move --diff` validates the source delimiters and target anchors, hashes the selected payload, self-checks the rendered patch against the current tree in memory, and prints it without writing to the tree.
|
|
320
93
|
|
|
321
|
-
|
|
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
|
|
94
|
+
Review the patch, then apply it:
|
|
329
95
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
+second line
|
|
96
|
+
```sh
|
|
97
|
+
blockpatch apply patch.blockpatch
|
|
333
98
|
```
|
|
334
99
|
|
|
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
100
|
```
|
|
348
|
-
|
|
349
|
-
|
|
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:
|
|
408
|
-
|
|
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> ]
|
|
101
|
+
changed src/foo.ts
|
|
102
|
+
changed src/bar.ts
|
|
425
103
|
```
|
|
426
104
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
Cross-file move:
|
|
105
|
+
`apply` checks the payload hash, requires the source block and target anchors to each match exactly once in the current tree, and replaces each changed file atomically. The tree now matches the requested final state:
|
|
430
106
|
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
+++ b/<source-path>
|
|
107
|
+
```ts
|
|
108
|
+
// after: src/foo.ts
|
|
109
|
+
export function keepThing() {
|
|
110
|
+
return 7;
|
|
111
|
+
}
|
|
437
112
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
-<moved payload>
|
|
441
|
-
[ <source context after> ]
|
|
113
|
+
// after: src/bar.ts
|
|
114
|
+
export const target = "here";
|
|
442
115
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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> ]
|
|
116
|
+
export function movedThing() {
|
|
117
|
+
return 42;
|
|
118
|
+
}
|
|
453
119
|
```
|
|
454
120
|
|
|
455
|
-
|
|
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.
|
|
121
|
+
Retrying the same `patch.blockpatch` against this tree reports `already_applied`: the patch carries the payload and hash, so a retry validates the final state instead of reselecting bytes from a changed tree.
|
|
458
122
|
|
|
459
|
-
The
|
|
123
|
+
The same handshake covers whole-file creation and removal via `mode: "create_file"` and `mode: "remove_file"`; those patches use strict `/dev/null` file headers.
|
|
460
124
|
|
|
461
|
-
|
|
125
|
+
## Common Commands
|
|
462
126
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
context after
|
|
127
|
+
```sh
|
|
128
|
+
blockpatch check patch.blockpatch
|
|
129
|
+
blockpatch apply patch.blockpatch --dry-run
|
|
130
|
+
blockpatch apply patch.blockpatch
|
|
131
|
+
blockpatch plan --json -
|
|
132
|
+
blockpatch move --json -
|
|
470
133
|
```
|
|
471
134
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
## Semantics
|
|
475
|
-
|
|
476
|
-
For one patch:
|
|
135
|
+
`check` parses a patch and verifies it against the target tree without writing. `apply --dry-run` validates through the apply path without writing. `apply` writes the verified result.
|
|
477
136
|
|
|
478
|
-
|
|
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.
|
|
137
|
+
`move --json -` applies a move request directly; with `--diff` it only prints the rendered patch. `plan --json -` runs the same planner but returns a JSON envelope with validation metadata and the patch in its `patch` field — the agent-facing form shown in [Commands](docs/commands.md). JSON over stdin is the most reliable form because it avoids shell quoting problems.
|
|
490
138
|
|
|
491
|
-
|
|
139
|
+
When using `--cwd`, operation paths inside patches and move JSON are relative to `--cwd`; input patch and move JSON filenames are normal CLI paths, relative to your shell working directory unless absolute.
|
|
492
140
|
|
|
493
|
-
|
|
141
|
+
## Docs
|
|
494
142
|
|
|
495
|
-
|
|
143
|
+
- [Commands](docs/commands.md): supported CLI forms and flags.
|
|
144
|
+
- [Patch spec](docs/spec.md): canonical `.blockpatch` artifact format, hunk syntax, `/dev/null`, byte rules, move JSON requests, JSON output, and error codes.
|
|
145
|
+
- [Behavior](docs/behavior.md): exact matching, idempotence, path containment, failure rules, and write behavior.
|
|
146
|
+
- [Conformance](conformance/): runnable `.blockpatch` cases for checking another implementation.
|
|
496
147
|
|
|
497
|
-
##
|
|
148
|
+
## Examples
|
|
498
149
|
|
|
499
|
-
|
|
150
|
+
- [same-file relocation](examples/same-file-relocation/)
|
|
151
|
+
- [cross-file relocation](examples/cross-file-relocation/)
|
|
152
|
+
- [insert existing file](examples/insert-existing-file/)
|
|
153
|
+
- [delete existing file](examples/delete-existing-file/)
|
|
154
|
+
- [create file](examples/create-file/)
|
|
155
|
+
- [remove file](examples/remove-file/)
|
|
156
|
+
- [reverse](examples/reverse/)
|
|
157
|
+
- [failure: ambiguous target](examples/failure-ambiguous-target/)
|
|
500
158
|
|
|
501
|
-
|
|
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
|
|
508
|
-
- source anchors are missing
|
|
509
|
-
- source anchors or full source are ambiguous
|
|
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
|
|
514
|
-
- the target anchor is missing
|
|
515
|
-
- the target anchor is ambiguous
|
|
516
|
-
- the target anchor overlaps the source payload
|
|
517
|
-
- file I/O fails before a write completes
|
|
159
|
+
Each example has a `patch.blockpatch`, a runnable `work/` directory, and `expected/` output for successful cases.
|
|
518
160
|
|
|
519
|
-
##
|
|
161
|
+
## Conformance
|
|
520
162
|
|
|
521
|
-
|
|
163
|
+
Run the published conformance cases against a blockpatch-compatible CLI:
|
|
522
164
|
|
|
523
|
-
|
|
524
|
-
-
|
|
525
|
-
|
|
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.
|
|
165
|
+
```sh
|
|
166
|
+
npx -p blockpatch blockpatch-conformance ./my-implementation
|
|
167
|
+
```
|
|
530
168
|
|
|
531
|
-
|
|
169
|
+
The runner checks apply/check behavior, retry idempotence, reverse application, byte preservation, and expected structured failures.
|
|
532
170
|
|
|
533
|
-
##
|
|
171
|
+
## Scope
|
|
534
172
|
|
|
535
|
-
|
|
173
|
+
`blockpatch` intentionally does not implement fuzzy matching, AST parsing, code formatting, copy operations, regex anchors, multiple independent moves in one patch document, or arbitrary generated diffs from before/after snapshots.
|
|
536
174
|
|
|
537
|
-
-
|
|
538
|
-
- arbitrary generated diffs from before/after file snapshots
|
|
539
|
-
- fuzzy matching
|
|
540
|
-
- AST parsing
|
|
541
|
-
- code formatting
|
|
542
|
-
- copy operations
|
|
543
|
-
- regex anchors
|
|
175
|
+
`apply` preserves patch body bytes exactly, but `plan`/`move --json` are text-oriented and intended for source files, not arbitrary binary payloads: JSON requests and rendered patches are UTF-8.
|