blockpatch 0.1.0 → 1.1.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 +26 -0
- package/README.md +131 -74
- 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 +2908 -244
- package/docs/behavior.md +207 -0
- package/docs/commands.md +84 -0
- package/docs/spec.md +356 -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 +33 -13
- 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/docs/behavior.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# blockpatch Behavior
|
|
2
|
+
|
|
3
|
+
`blockpatch` is intentionally strict. It prefers refusing a patch over guessing, regenerating bytes, or applying a fuzzy edit.
|
|
4
|
+
|
|
5
|
+
For CLI forms, see [Commands](commands.md). For the `.blockpatch` artifact format and JSON contracts, see [Patch spec](spec.md).
|
|
6
|
+
|
|
7
|
+
## Core Invariant
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
The payload hash is checked before any write happens. For paired moves, the source `-` payload and target `+` payload must be byte-identical.
|
|
12
|
+
|
|
13
|
+
## Planning And Retry Flow
|
|
14
|
+
|
|
15
|
+
`plan --json -` is the canonical planning handshake. It 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.
|
|
16
|
+
|
|
17
|
+
`move --json --diff` is a planner for the current tree. For relocation, in-file deletion, and whole-file removal, the JSON request selects the payload from the current source file; if that source block or file 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 and `create_file` JSON are the exceptions because they include `payload` directly.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
## Path Containment
|
|
22
|
+
|
|
23
|
+
Patch-declared source and destination paths, and move JSON `src`/`dst` paths, must be relative, non-empty, and resolve inside `--cwd`.
|
|
24
|
+
|
|
25
|
+
Rejected operation paths include:
|
|
26
|
+
|
|
27
|
+
- absolute paths
|
|
28
|
+
- `.` or `..` path segments
|
|
29
|
+
- backslashes; operation paths use POSIX-style `/` separators in `.blockpatch` artifacts and move JSON
|
|
30
|
+
- non-printing control characters
|
|
31
|
+
- paths containing symlink components
|
|
32
|
+
- existing regular files whose real path escapes `--cwd`
|
|
33
|
+
|
|
34
|
+
Patch files and move JSON files may be read from any path; use `--cwd` to choose the directory the operation is allowed to modify.
|
|
35
|
+
|
|
36
|
+
Relative patch file paths and relative move JSON file paths resolve from the shell working directory, not from `--cwd`. Operation paths declared inside a patch or move JSON request resolve inside `--cwd`.
|
|
37
|
+
|
|
38
|
+
`/dev/null` is reserved for path absence and is never resolved, opened, or checked against `--cwd`. 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.
|
|
39
|
+
|
|
40
|
+
## Exact Matching
|
|
41
|
+
|
|
42
|
+
Source and target anchors are byte-exact. There is no fuzzy matching, regex matching, AST parsing, or formatting.
|
|
43
|
+
|
|
44
|
+
For source hunks, `blockpatch` locates exactly one source match for:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
source context before + payload + source context after
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For target hunks in existing files, `blockpatch` locates exactly one target match for:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
target context before + target context after
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For target hunks in existing files, insertion occurs between target-before and target-after context:
|
|
57
|
+
|
|
58
|
+
```diff
|
|
59
|
+
@@ -40,2 +40,3 @@ blockpatch-target id=move-1
|
|
60
|
+
context before
|
|
61
|
+
+moved payload
|
|
62
|
+
context after
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The moved bytes are extracted from the source file, not regenerated from arguments. `apply` and `check` preserve parsed hunk body bytes exactly, including CRLF and no-trailing-newline cases.
|
|
66
|
+
|
|
67
|
+
## Move JSON Behavior
|
|
68
|
+
|
|
69
|
+
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`.
|
|
70
|
+
|
|
71
|
+
For source selection, each `src_start` match pairs with the first `src_end` occurrence after it. The resulting source delimiter match must be unique.
|
|
72
|
+
|
|
73
|
+
For target placement:
|
|
74
|
+
|
|
75
|
+
- insertion is between the before and after contexts, and their concatenation must match exactly once.
|
|
76
|
+
- if only `target_before` is supplied, insertion is immediately after that context.
|
|
77
|
+
- if only `target_after` is supplied, insertion is immediately before that context.
|
|
78
|
+
- either target side may be empty when both are supplied, but not both may be empty.
|
|
79
|
+
|
|
80
|
+
When `expected_payload_sha256` is supplied, the moved or materialized payload bytes must hash to that value before any write happens.
|
|
81
|
+
|
|
82
|
+
Same-file source and target overlap is a hard failure.
|
|
83
|
+
|
|
84
|
+
`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.
|
|
85
|
+
|
|
86
|
+
## Patch Evaluation
|
|
87
|
+
|
|
88
|
+
For one patch, `blockpatch`:
|
|
89
|
+
|
|
90
|
+
1. Parses one same-file section with source+target, source-only, or target-only hunks; or parses two cross-file relocation sections tied by `role=source` and `role=target`.
|
|
91
|
+
2. Verifies hunk ids match `blockpatch move id=<id>`.
|
|
92
|
+
3. Extracts source payload from contiguous `-` lines when a source hunk exists.
|
|
93
|
+
4. Extracts target payload from contiguous `+` lines when a target hunk exists.
|
|
94
|
+
5. For paired moves, verifies target payload exactly equals source payload.
|
|
95
|
+
6. Verifies `payload-sha256` matches the exact moved or materialized payload bytes.
|
|
96
|
+
7. For source hunks, locates exactly one source match for `source context before + payload + source context after`.
|
|
97
|
+
8. For target hunks in existing files, locates exactly one target match for `target context before + target context after`.
|
|
98
|
+
9. Fails if a same-file target context range overlaps the source payload bytes.
|
|
99
|
+
10. Applies the hunk transition: remove source payload, insert target payload, or both.
|
|
100
|
+
11. Applies any path-state transition from `/dev/null`: create the missing destination path or remove the source path.
|
|
101
|
+
12. Writes changed files with temp-file-and-rename replacement.
|
|
102
|
+
|
|
103
|
+
Line-number ranges are review hints only. `blockpatch` validates hunk body line counts, but it locates changes by exact context and payload bytes.
|
|
104
|
+
|
|
105
|
+
## One-Sided And Null-Endpoint Behavior
|
|
106
|
+
|
|
107
|
+
Rules for target-only insertion into an existing file:
|
|
108
|
+
|
|
109
|
+
- the section contains exactly one `blockpatch-target` hunk and no source hunk.
|
|
110
|
+
- the payload comes from the `+` lines and must match `payload-sha256`.
|
|
111
|
+
- the file must exist; missing files are not treated as empty files.
|
|
112
|
+
- `target context before + target context after` must match exactly once; the payload is inserted at the boundary.
|
|
113
|
+
- at least one side of target context is required, so arbitrary zero-byte in-file insertions are not valid.
|
|
114
|
+
- if `target before + payload + target after` is already present exactly once, the result is `already_applied`.
|
|
115
|
+
|
|
116
|
+
Rules for source-only deletion from an existing file:
|
|
117
|
+
|
|
118
|
+
- the section contains exactly one `blockpatch-source` hunk and no target hunk.
|
|
119
|
+
- the file must exist; missing files are not treated as empty files.
|
|
120
|
+
- `source context before + payload + source context after` must match exactly once; the payload bytes are removed.
|
|
121
|
+
- if removal leaves zero bytes, the file remains as an empty file.
|
|
122
|
+
- retries are idempotent when both source anchors are adjacent, when the remaining after anchor is at the start of the file, when the remaining before anchor is at the end of the file, or when an anchorless payload is absent.
|
|
123
|
+
|
|
124
|
+
Rules for `/dev/null -> file` creation:
|
|
125
|
+
|
|
126
|
+
- the section contains exactly one `blockpatch-target` hunk and no source hunk.
|
|
127
|
+
- the target hunk must be whole-file payload: only contiguous `+` payload lines, with no context lines.
|
|
128
|
+
- zero-byte payload is valid and creates an empty file.
|
|
129
|
+
- a missing destination is created, including parent directories; new files are created with mode 0644.
|
|
130
|
+
- if the destination already exists with exactly the requested bytes, the result is `already_applied`; if it exists with different bytes, the patch fails.
|
|
131
|
+
|
|
132
|
+
Rules for `file -> /dev/null` removal:
|
|
133
|
+
|
|
134
|
+
- the section contains exactly one `blockpatch-source` hunk and no target hunk.
|
|
135
|
+
- the source hunk must be whole-file payload: only contiguous `-` payload lines, with no context lines.
|
|
136
|
+
- zero-byte payload is valid and removes an empty file.
|
|
137
|
+
- the existing file bytes must exactly equal the source payload; then the path is removed.
|
|
138
|
+
- if the source path is already missing, the result is `already_applied`.
|
|
139
|
+
|
|
140
|
+
## Idempotence
|
|
141
|
+
|
|
142
|
+
If the requested final state is already present, `blockpatch` reports `already_applied`.
|
|
143
|
+
|
|
144
|
+
For paired moves, that means the source full match is absent and:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
target context before + payload + target context after
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
is present exactly once.
|
|
151
|
+
|
|
152
|
+
For one-sided and null-endpoint shapes, the `already_applied` conditions are listed per shape in [One-Sided And Null-Endpoint Behavior](#one-sided-and-null-endpoint-behavior).
|
|
153
|
+
|
|
154
|
+
This is strict retry idempotence. It does not search fuzzily or infer moved bytes.
|
|
155
|
+
|
|
156
|
+
## Reverse Application
|
|
157
|
+
|
|
158
|
+
`-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.
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
## Write Behavior
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
Before renaming or removing a path, `blockpatch` re-checks that every existing file still matches the bytes and stat captured during planning. For path creation, it re-checks that the destination is still absent or already contains the exact expected bytes. If the live path no longer matches the verified input state, the operation fails with `concurrent_modification`.
|
|
167
|
+
|
|
168
|
+
Once renames begin, a two-file operation is 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.
|
|
169
|
+
|
|
170
|
+
If a retry sees that a cross-file source payload is still present while the destination already contains the exact final target state, `blockpatch` fails with `partial_applied_duplicate` instead of guessing. The error includes the source range, target range, payload hash, and `suggested_action: "review_then_remove_source"`.
|
|
171
|
+
|
|
172
|
+
## Failure Rules
|
|
173
|
+
|
|
174
|
+
`blockpatch` exits non-zero and does not modify files when:
|
|
175
|
+
|
|
176
|
+
- the patch file is malformed
|
|
177
|
+
- both endpoints are `/dev/null`
|
|
178
|
+
- a patch-declared or move-declared source/destination path is absolute, invalid, or escapes `--cwd`
|
|
179
|
+
- a referenced file is missing, unless the patch explicitly says `/dev/null` where creation or removal would make that legal
|
|
180
|
+
- a referenced path is not a regular file, unreadable, unwritable, or otherwise hits a filesystem error
|
|
181
|
+
- a target-only existing-file insertion has no target context
|
|
182
|
+
- a `/dev/null -> file` creation targets an existing file with different bytes
|
|
183
|
+
- a `file -> /dev/null` removal hunk does not match the whole file
|
|
184
|
+
- source anchors are missing
|
|
185
|
+
- source anchors or full source are ambiguous
|
|
186
|
+
- a cross-file retry finds the source payload still present and the destination already patched
|
|
187
|
+
- the located source payload does not exactly match the source hunk payload
|
|
188
|
+
- target added payload does not exactly match source removed payload
|
|
189
|
+
- `payload-sha256` does not match the moved payload
|
|
190
|
+
- `move --diff` would need to render invalid UTF-8 bytes
|
|
191
|
+
- the target anchor is missing
|
|
192
|
+
- the target anchor is ambiguous
|
|
193
|
+
- the target anchor overlaps the source payload
|
|
194
|
+
- a file changes after `blockpatch` verifies it but before the planned write/remove
|
|
195
|
+
- file I/O fails before a write completes
|
|
196
|
+
|
|
197
|
+
## Intentionally Out Of Scope
|
|
198
|
+
|
|
199
|
+
`blockpatch` does not implement:
|
|
200
|
+
|
|
201
|
+
- multiple independent moves in one patch document
|
|
202
|
+
- arbitrary generated diffs from before/after file snapshots
|
|
203
|
+
- fuzzy matching
|
|
204
|
+
- AST parsing
|
|
205
|
+
- code formatting
|
|
206
|
+
- copy operations
|
|
207
|
+
- regex anchors
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# blockpatch Commands
|
|
2
|
+
|
|
3
|
+
This document lists the supported CLI forms. For `.blockpatch`, move JSON, and JSON output contracts, see [Patch spec](spec.md). For planning, matching, idempotence, and write behavior, see [Behavior](behavior.md).
|
|
4
|
+
|
|
5
|
+
## Common Commands
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
blockpatch check patch.blockpatch
|
|
9
|
+
blockpatch apply patch.blockpatch --dry-run
|
|
10
|
+
blockpatch apply patch.blockpatch
|
|
11
|
+
blockpatch plan --json -
|
|
12
|
+
blockpatch move --json -
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`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. `plan` returns a reviewable `.blockpatch` for a move JSON request without writing; `move` applies the request directly.
|
|
16
|
+
|
|
17
|
+
## Patch Input
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
blockpatch apply - < patch.blockpatch
|
|
21
|
+
blockpatch apply < patch.blockpatch
|
|
22
|
+
blockpatch apply -i patch.blockpatch
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`apply` and `check` read the patch from stdin when no patch path is supplied. `-i`/`--input` names the patch file explicitly.
|
|
26
|
+
|
|
27
|
+
## Paths And Stripping
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
blockpatch apply -d repo-root -p1 patch.blockpatch
|
|
31
|
+
blockpatch check --cwd repo-root --strip 1 < patch.blockpatch
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`-d`/`--directory` is an alias for `--cwd`. `-pN`/`--strip N` strips leading path components from patch-declared file paths. Unlike GNU `patch`, the default is `-p1`, matching the git-style `a/`/`b/` prefixes; use `-p0` only if your working tree contains literal `a/` and `b/` directories.
|
|
35
|
+
|
|
36
|
+
## Reverse
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
blockpatch apply patch.blockpatch --reverse
|
|
40
|
+
blockpatch check patch.blockpatch -R
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`-R`/`--reverse` works with both `check` and `apply`.
|
|
44
|
+
|
|
45
|
+
## Move JSON Input
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
blockpatch plan --json -
|
|
49
|
+
blockpatch move --json -
|
|
50
|
+
blockpatch move --json - --diff --json-output
|
|
51
|
+
blockpatch move --json move.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--json -` reads the move request from stdin; `--json move.json` reads the same shape from a file. `plan` is shorthand for `move --json - --diff --json-output`: it validates the request and returns the rendered `.blockpatch` in the JSON `patch` field without writing. On `move`, `--dry-run` validates without writing, and `--diff` never writes: it prints the rendered patch, or returns it in the JSON `patch` field with `--json-output`.
|
|
55
|
+
|
|
56
|
+
## Plan Envelope
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
blockpatch plan --json - < move.json > plan.json
|
|
60
|
+
jq -r .patch plan.json > patch.blockpatch
|
|
61
|
+
blockpatch apply patch.blockpatch
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`plan` writes a single-line JSON envelope to stdout: `ok`, `changed`, `affected`, `status`, per-move byte ranges, and the rendered patch in the `patch` field, per the contract in [Patch spec](spec.md#json-output). Extract the `patch` field to save the reviewable artifact; `apply` accepts only the `.blockpatch`, never the envelope.
|
|
65
|
+
|
|
66
|
+
## Move Flags
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
blockpatch move --src src/foo.ts --src-start $'\nfunction movedThing() {' --src-end $'\n}\n' --target-before $'class Target {\n'
|
|
70
|
+
blockpatch move --src /dev/null --dst src/foo.ts --payload $'inserted bytes\n' --target-before $'context before\n'
|
|
71
|
+
blockpatch move --src src/foo.ts --src-start $'\nfunction removeMe() {' --src-end $'\n}\n' --dst /dev/null
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each flag sets the move JSON field of the same name and cannot be combined with `--json`. `mode` has no flag form, so whole-file `create_file`/`remove_file` requests are JSON-only. JSON avoids shell quoting problems, so it is usually the more reliable form for agents.
|
|
75
|
+
|
|
76
|
+
## Output
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
blockpatch apply patch.blockpatch --json-output
|
|
80
|
+
blockpatch apply patch.blockpatch --explain
|
|
81
|
+
blockpatch version --json-output
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Without `--json-output`, successful commands print `changed <path>`, `would change <path>`, or `unchanged <path>`. `--json-output` switches success and error reporting to the JSON contract in [Patch spec](spec.md#json-output); `plan` always uses it. `--explain` implies `--json-output` plus, for `apply`, `move`, and `plan`, `--dry-run`, so it always reports without writing. `version` prints the CLI version.
|
package/docs/spec.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# blockpatch Spec
|
|
2
|
+
|
|
3
|
+
This document defines the `.blockpatch` artifact format, move JSON request contract, and JSON output contract. The public API is the CLI and its JSON output; there is no library API.
|
|
4
|
+
|
|
5
|
+
For command forms, see [Commands](commands.md). For planning, matching, idempotence, and write behavior, see [Behavior](behavior.md).
|
|
6
|
+
|
|
7
|
+
## Format
|
|
8
|
+
|
|
9
|
+
```diff
|
|
10
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
11
|
+
blockpatch version 1
|
|
12
|
+
blockpatch move id=move-1 payload-sha256=bc8a95d6eb2b44aa564dbae1040ba8ff2273988ea43f0f3b0c47228f9dba6b3d
|
|
13
|
+
--- a/src/example.ts
|
|
14
|
+
+++ b/src/example.ts
|
|
15
|
+
|
|
16
|
+
@@ -1,8 +1,4 @@ blockpatch-source id=move-1 function movedThing
|
|
17
|
+
function alpha() {
|
|
18
|
+
}
|
|
19
|
+
-
|
|
20
|
+
-function movedThing() {
|
|
21
|
+
- console.log("keep me exact");
|
|
22
|
+
-}
|
|
23
|
+
function omega() {
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@@ -40,3 +36,7 @@ blockpatch-target id=move-1 constructor
|
|
27
|
+
constructor() {
|
|
28
|
+
}
|
|
29
|
+
+
|
|
30
|
+
+function movedThing() {
|
|
31
|
+
+ console.log("keep me exact");
|
|
32
|
+
+}
|
|
33
|
+
methodAfter() {
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Same-file moves, insertions, and deletions use one file section. In that shape, the `--- a/<path>` and `+++ b/<path>` headers must name the same file after normal path cleanup.
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
diff --blockpatch a/<path> b/<path>
|
|
40
|
+
blockpatch version 1
|
|
41
|
+
blockpatch move id=<id> payload-sha256=<sha256>
|
|
42
|
+
--- a/<path>
|
|
43
|
+
+++ b/<path>
|
|
44
|
+
|
|
45
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-source id=<id> optional label
|
|
46
|
+
[ <source context before> ]
|
|
47
|
+
-<moved payload>
|
|
48
|
+
[ <source context after> ]
|
|
49
|
+
|
|
50
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-target id=<id> optional label
|
|
51
|
+
[ <target context before> ]
|
|
52
|
+
+<same moved payload>
|
|
53
|
+
[ <target context after> ]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
diff --blockpatch a/<source-path> b/<source-path>
|
|
62
|
+
blockpatch version 1
|
|
63
|
+
blockpatch move id=<id> role=source payload-sha256=<sha256>
|
|
64
|
+
--- a/<source-path>
|
|
65
|
+
+++ b/<source-path>
|
|
66
|
+
|
|
67
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-source id=<id> optional label
|
|
68
|
+
[ <source context before> ]
|
|
69
|
+
-<moved payload>
|
|
70
|
+
[ <source context after> ]
|
|
71
|
+
|
|
72
|
+
diff --blockpatch a/<target-path> b/<target-path>
|
|
73
|
+
blockpatch version 1
|
|
74
|
+
blockpatch move id=<id> role=target payload-sha256=<sha256>
|
|
75
|
+
--- a/<target-path>
|
|
76
|
+
+++ b/<target-path>
|
|
77
|
+
|
|
78
|
+
@@ -<old-start>,<old-count> +<new-start>,<new-count> @@ blockpatch-target id=<id> optional label
|
|
79
|
+
[ <target context before> ]
|
|
80
|
+
+<same moved payload>
|
|
81
|
+
[ <target context after> ]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Format constraints:
|
|
85
|
+
|
|
86
|
+
- Every file section must declare `blockpatch version 1` on the line after `diff --blockpatch`.
|
|
87
|
+
- 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. The prefixes are consumed by the default `-p1` path stripping ([Commands](commands.md#paths-and-stripping)).
|
|
88
|
+
- Patch-declared paths use POSIX-style `/` separators on every platform. Backslashes, `.`/`..` path segments, and non-printing control characters are rejected instead of normalized or escaped.
|
|
89
|
+
- `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.
|
|
90
|
+
- Source context before and after may each be empty.
|
|
91
|
+
- Target hunks for existing files must include context on at least one side; either side may be empty, but not both.
|
|
92
|
+
- Whole-file creation and removal hunks (a `/dev/null` endpoint) contain only contiguous payload lines, with no context lines.
|
|
93
|
+
- The `-<old-start>,<old-count> +<new-start>,<new-count>` ranges must match the hunk body line counts, but the range values are line-number hints for review.
|
|
94
|
+
|
|
95
|
+
## One-Sided Hunks And Null Endpoints
|
|
96
|
+
|
|
97
|
+
One-sided hunks are for in-file insertion and deletion when the file exists both before and after the patch.
|
|
98
|
+
|
|
99
|
+
Target-only insertion into an existing file:
|
|
100
|
+
|
|
101
|
+
```diff
|
|
102
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
103
|
+
blockpatch version 1
|
|
104
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the added payload>
|
|
105
|
+
--- a/src/example.ts
|
|
106
|
+
+++ b/src/example.ts
|
|
107
|
+
|
|
108
|
+
@@ -1,2 +1,3 @@ blockpatch-target id=move-1
|
|
109
|
+
context before
|
|
110
|
+
+inserted line
|
|
111
|
+
context after
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Source-only deletion from an existing file:
|
|
115
|
+
|
|
116
|
+
```diff
|
|
117
|
+
diff --blockpatch a/src/example.ts b/src/example.ts
|
|
118
|
+
blockpatch version 1
|
|
119
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the removed payload>
|
|
120
|
+
--- a/src/example.ts
|
|
121
|
+
+++ b/src/example.ts
|
|
122
|
+
|
|
123
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
124
|
+
context before
|
|
125
|
+
-doomed line
|
|
126
|
+
context after
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`/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`).
|
|
130
|
+
|
|
131
|
+
Use `/dev/null` only when the file path itself is absent before or after the patch. A `/dev/null -> file` section is the whole-file creation shape:
|
|
132
|
+
|
|
133
|
+
```diff
|
|
134
|
+
diff --blockpatch /dev/null b/src/new.txt
|
|
135
|
+
blockpatch version 1
|
|
136
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the file payload>
|
|
137
|
+
--- /dev/null
|
|
138
|
+
+++ b/src/new.txt
|
|
139
|
+
|
|
140
|
+
@@ -0,0 +1,2 @@ blockpatch-target id=move-1
|
|
141
|
+
+first line
|
|
142
|
+
+second line
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A `file -> /dev/null` section is the whole-file removal shape:
|
|
146
|
+
|
|
147
|
+
```diff
|
|
148
|
+
diff --blockpatch a/src/old.txt /dev/null
|
|
149
|
+
blockpatch version 1
|
|
150
|
+
blockpatch move id=move-1 payload-sha256=<sha256 of the file payload>
|
|
151
|
+
--- a/src/old.txt
|
|
152
|
+
+++ /dev/null
|
|
153
|
+
|
|
154
|
+
@@ -1,2 +0,0 @@ blockpatch-source id=move-1
|
|
155
|
+
-first line
|
|
156
|
+
-second line
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The section shapes and their meaning:
|
|
160
|
+
|
|
161
|
+
| Shape | Meaning |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `file -> file`, source + target hunks | relocation |
|
|
164
|
+
| `file -> file`, target hunk only | insert payload into an existing file |
|
|
165
|
+
| `file -> file`, source hunk only | delete payload from an existing file |
|
|
166
|
+
| `/dev/null -> file`, target hunk only | create a file |
|
|
167
|
+
| `file -> /dev/null`, source hunk only | remove a file |
|
|
168
|
+
| `/dev/null -> /dev/null` | invalid |
|
|
169
|
+
|
|
170
|
+
Matching rules, idempotence, reverse application, and write behavior are documented in [Behavior](behavior.md).
|
|
171
|
+
|
|
172
|
+
## Byte Rules
|
|
173
|
+
|
|
174
|
+
Hunk body lines use unified-diff prefixes:
|
|
175
|
+
|
|
176
|
+
- space for context
|
|
177
|
+
- `-` for source payload
|
|
178
|
+
- `+` for target payload
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
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.
|
|
183
|
+
|
|
184
|
+
## Move JSON
|
|
185
|
+
|
|
186
|
+
Move JSON is the request contract accepted by `blockpatch plan --json` and `blockpatch move --json`.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
type MoveBlockArgs = {
|
|
190
|
+
src: string
|
|
191
|
+
src_start?: string
|
|
192
|
+
src_end?: string
|
|
193
|
+
dst?: string
|
|
194
|
+
payload?: string
|
|
195
|
+
target_before?: string
|
|
196
|
+
target_after?: string
|
|
197
|
+
expected_payload_sha256?: string
|
|
198
|
+
mode?: "create_file" | "remove_file"
|
|
199
|
+
dry_run?: boolean
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Request shapes:
|
|
204
|
+
|
|
205
|
+
- For relocation, `src_start` and `src_end` are inclusive source delimiters, and `dst` defaults to `src`.
|
|
206
|
+
- For deletion, set `dst` to `/dev/null`; `src_start` and `src_end` select the removed payload.
|
|
207
|
+
- For insertion, set `src` to `/dev/null`; `dst`, `payload`, and target context are required.
|
|
208
|
+
- For file creation, set `src` to `/dev/null`, set `mode` to `create_file`, and provide `dst` plus `payload`. Empty payload is valid and creates an empty file.
|
|
209
|
+
- For file removal, set `dst` to `/dev/null` and set `mode` to `remove_file`. The whole source file is selected as the payload.
|
|
210
|
+
- `mode` selects the whole-file path shapes; without it, `/dev/null` endpoints denote in-file insertion/deletion. `move --diff` renders the in-file shapes as same-file one-sided sections and the `mode` shapes with `/dev/null` file headers.
|
|
211
|
+
- `target_before`, `target_after`, or both are required for relocation and insertion.
|
|
212
|
+
- `payload` is only valid when `src` is `/dev/null`; it must be non-empty for in-file insertion.
|
|
213
|
+
- `expected_payload_sha256` is optional.
|
|
214
|
+
|
|
215
|
+
Insertion:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"src": "/dev/null",
|
|
220
|
+
"dst": "src/foo.ts",
|
|
221
|
+
"payload": "inserted bytes\n",
|
|
222
|
+
"target_before": "context before\n"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Deletion:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"src": "src/foo.ts",
|
|
231
|
+
"src_start": "function removeMe() {\n",
|
|
232
|
+
"src_end": "}\n",
|
|
233
|
+
"dst": "/dev/null"
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
File creation:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"src": "/dev/null",
|
|
242
|
+
"dst": "src/new.ts",
|
|
243
|
+
"payload": "export const x = 1;\n",
|
|
244
|
+
"mode": "create_file"
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
File removal:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"src": "src/old.ts",
|
|
253
|
+
"dst": "/dev/null",
|
|
254
|
+
"mode": "remove_file",
|
|
255
|
+
"expected_payload_sha256": "<sha256>"
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Matching behavior for move JSON requests is documented in [Behavior](behavior.md#move-json-behavior).
|
|
260
|
+
|
|
261
|
+
## JSON Output
|
|
262
|
+
|
|
263
|
+
With `--json-output`, successful commands print:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
type ApplyResult = {
|
|
267
|
+
ok: true
|
|
268
|
+
changed: string[]
|
|
269
|
+
affected: string[]
|
|
270
|
+
written: boolean
|
|
271
|
+
noop: boolean
|
|
272
|
+
status: "applied" | "noop" | "already_applied"
|
|
273
|
+
strip_components?: number
|
|
274
|
+
patch?: string
|
|
275
|
+
moves: Array<{
|
|
276
|
+
id: string
|
|
277
|
+
src: string
|
|
278
|
+
dst: string
|
|
279
|
+
payload_sha256: string
|
|
280
|
+
payload_bytes: number
|
|
281
|
+
source_range: { start: number; end: number } | null
|
|
282
|
+
target_range: { start: number; end: number } | null
|
|
283
|
+
insert_index: number | null
|
|
284
|
+
}>
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`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.
|
|
289
|
+
|
|
290
|
+
`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`.
|
|
291
|
+
|
|
292
|
+
`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`.
|
|
293
|
+
|
|
294
|
+
With `--json-output`, errors print:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
type BlockPatchJsonError = {
|
|
298
|
+
ok: false
|
|
299
|
+
error: {
|
|
300
|
+
code: BlockPatchErrorCode
|
|
301
|
+
message: string
|
|
302
|
+
field?: string
|
|
303
|
+
path?: string
|
|
304
|
+
phase?: string
|
|
305
|
+
anchor?: string
|
|
306
|
+
matches?: number
|
|
307
|
+
matches_truncated?: boolean
|
|
308
|
+
ranges?: Array<{ start: number; end: number }>
|
|
309
|
+
line_ranges?: Array<{ start: number; end: number }>
|
|
310
|
+
source_range?: { start: number; end: number }
|
|
311
|
+
target_range?: { start: number; end: number }
|
|
312
|
+
payload_sha256?: string
|
|
313
|
+
suggested_action?: string
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
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. When `matches_truncated` is true, `matches` is a lower bound rather than an exact count. They do not include source snippets, fuzzy suggestions, or repair guidance.
|
|
319
|
+
|
|
320
|
+
`partial_applied_duplicate` reports an interrupted cross-file apply state where the source payload is still present and the destination already contains the exact final target state. It includes `source_range`, `target_range`, `payload_sha256`, and `suggested_action: "review_then_remove_source"`. `blockpatch` does not auto-repair this state.
|
|
321
|
+
|
|
322
|
+
Error codes are the agent-facing branch contract: branch on `error.code`, not on human-readable messages. Removing a code or changing its meaning is semver-major.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
type BlockPatchErrorCode =
|
|
326
|
+
| "parse_error"
|
|
327
|
+
| "invalid_path"
|
|
328
|
+
| "path_outside_cwd"
|
|
329
|
+
| "symlink_path"
|
|
330
|
+
| "file_not_found"
|
|
331
|
+
| "not_regular_file"
|
|
332
|
+
| "permission_denied"
|
|
333
|
+
| "io_error"
|
|
334
|
+
| "source_not_found"
|
|
335
|
+
| "source_ambiguous"
|
|
336
|
+
| "target_not_found"
|
|
337
|
+
| "target_ambiguous"
|
|
338
|
+
| "destination_exists"
|
|
339
|
+
| "concurrent_modification"
|
|
340
|
+
| "partial_applied_duplicate"
|
|
341
|
+
| "payload_mismatch"
|
|
342
|
+
| "hash_mismatch"
|
|
343
|
+
| "invalid_utf8"
|
|
344
|
+
| "target_overlaps_source"
|
|
345
|
+
| "invalid_move_args"
|
|
346
|
+
| "invalid_json"
|
|
347
|
+
| "missing_move_args"
|
|
348
|
+
| "unknown_command"
|
|
349
|
+
| "unknown_option"
|
|
350
|
+
| "invalid_option"
|
|
351
|
+
| "missing_option_value"
|
|
352
|
+
| "too_many_args"
|
|
353
|
+
| "unexpected_error"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`unexpected_error` is the generic fallback for non-`BlockPatchError` failures; agents should treat it as an internal failure and avoid branching on its message.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Each successful example contains:
|
|
4
|
+
|
|
5
|
+
- `patch.blockpatch`: the reviewed patch artifact.
|
|
6
|
+
- `work/`: the before-state tree used by `-d`.
|
|
7
|
+
- `expected/`: the after-state tree after applying the patch.
|
|
8
|
+
|
|
9
|
+
Check an example without writing:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
blockpatch check examples/same-file-relocation/patch.blockpatch -d examples/same-file-relocation/work
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To apply without changing the checked-in example tree, copy `work/` to a scratch directory and use that as `-d`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
created file
|