blockpatch 1.1.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # blockpatch
2
2
 
3
3
  [![CI](https://github.com/cheese-melted/blockpatch/actions/workflows/ci.yml/badge.svg)](https://github.com/cheese-melted/blockpatch/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/blockpatch.svg)](https://www.npmjs.com/package/blockpatch)
4
+ [![npm version](https://img.shields.io/npm/v/blockpatch.svg?cacheSeconds=300)](https://www.npmjs.com/package/blockpatch)
5
5
 
6
6
  Cut/paste for agents: hash-verified block moves that read like unified diffs.
7
7
 
package/dist/cli.js CHANGED
@@ -1953,7 +1953,8 @@ async function moveBlock(args, options = {}) {
1953
1953
  selection
1954
1954
  })
1955
1955
  ],
1956
- patch
1956
+ patch,
1957
+ warnings: insertionBoundaryWarnings(normalized.dst, normalized.targetBefore, selection.payload, normalized.targetAfter)
1957
1958
  };
1958
1959
  }
1959
1960
  function validateMoveArgs(value) {
@@ -2218,9 +2219,40 @@ async function insertPayload(args, validated, cwd, dryRun, options) {
2218
2219
  insert_index: target.insertIndex
2219
2220
  }
2220
2221
  ],
2221
- patch: renderedPatch
2222
+ patch: renderedPatch,
2223
+ warnings: insertionBoundaryWarnings(args.dst, args.targetBefore, args.payload, args.targetAfter)
2222
2224
  };
2223
2225
  }
2226
+ function insertionBoundaryWarnings(path, targetBefore, payload, targetAfter) {
2227
+ const warnings = [];
2228
+ if (targetBefore.length > 0 && payload.length > 0 && !endsWithLf(targetBefore) && !startsWithLf(payload)) {
2229
+ warnings.push({
2230
+ code: "adjacent_bytes",
2231
+ message: "Insertion will place payload immediately after target_before with no newline or separator inserted by blockpatch",
2232
+ path,
2233
+ phase: "target",
2234
+ boundary: "target_before+payload",
2235
+ suggested_action: "include the intended newline in target_before or at the start of payload"
2236
+ });
2237
+ }
2238
+ if (payload.length > 0 && targetAfter.length > 0 && !endsWithLf(payload) && !startsWithLf(targetAfter)) {
2239
+ warnings.push({
2240
+ code: "adjacent_bytes",
2241
+ message: "Insertion will place target_after immediately after payload with no newline or separator inserted by blockpatch",
2242
+ path,
2243
+ phase: "target",
2244
+ boundary: "payload+target_after",
2245
+ suggested_action: "include the intended newline at the end of payload or at the start of target_after"
2246
+ });
2247
+ }
2248
+ return warnings.length > 0 ? warnings : undefined;
2249
+ }
2250
+ function startsWithLf(bytes) {
2251
+ return bytes[0] === 10;
2252
+ }
2253
+ function endsWithLf(bytes) {
2254
+ return bytes[bytes.length - 1] === 10;
2255
+ }
2224
2256
  function verifyExpectedPayloadHash(args, payloadSha256) {
2225
2257
  if (args.expected_payload_sha256 !== undefined && args.expected_payload_sha256 !== payloadSha256) {
2226
2258
  fail("hash_mismatch", "expected_payload_sha256 does not match selected source payload", {
@@ -2671,12 +2703,25 @@ async function loadMoveArgs(options) {
2671
2703
  }
2672
2704
  const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.moveJsonPath), "move JSON file");
2673
2705
  try {
2674
- return JSON.parse(jsonBytes.toString("utf8"));
2706
+ const parsed = JSON.parse(jsonBytes.toString("utf8"));
2707
+ if (isEmptyObject(parsed)) {
2708
+ throw new BlockPatchError("invalid_move_args", "Move JSON cannot be empty; provide src plus source selectors or payload and target anchors", {
2709
+ field: "src",
2710
+ suggested_action: "Use fields like src, src_start, src_end, dst, target_before, target_after, payload, or run blockpatch help"
2711
+ });
2712
+ }
2713
+ return parsed;
2675
2714
  } catch (error) {
2715
+ if (error instanceof BlockPatchError) {
2716
+ throw error;
2717
+ }
2676
2718
  const message = error instanceof Error ? error.message : String(error);
2677
2719
  throw new BlockPatchError("invalid_json", `Invalid move JSON: ${message}`);
2678
2720
  }
2679
2721
  }
2722
+ function isEmptyObject(value) {
2723
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
2724
+ }
2680
2725
  function parseArgs(argv) {
2681
2726
  const args = [...argv];
2682
2727
  const outputFlags = takeLeadingOutputFlags(args);
@@ -2954,6 +2999,29 @@ Usage:
2954
2999
  blockpatch move --src /dev/null --dst <path> --payload <text> --target-before <text>
2955
3000
  blockpatch move --src <path> --src-start <text> --src-end <text> --dst /dev/null
2956
3001
  blockpatch version
3002
+
3003
+ Move JSON fields:
3004
+ src, dst, src_start, src_end, payload, target_before, target_after,
3005
+ expected_payload_sha256, mode, dry_run
3006
+
3007
+ Move selection:
3008
+ src_start/src_end are byte-exact, newline-sensitive delimiters. The selected
3009
+ payload starts at src_start and ends after the first following src_end.
3010
+
3011
+ Target anchors:
3012
+ target_before is the exact context immediately before the insertion point,
3013
+ so insertion occurs after it. target_after is the exact context immediately
3014
+ after the insertion point, so insertion occurs before it. With both anchors,
3015
+ insertion occurs between them.
3016
+
3017
+ Newlines:
3018
+ blockpatch never adds separators. Include every intended newline in
3019
+ src_start/src_end, payload, target_before, or target_after.
3020
+
3021
+ Examples:
3022
+ blockpatch move --json - --diff <<'JSON'
3023
+ {"src":"src/a.ts","src_start":"function x() {\\n","src_end":"}\\n","dst":"src/b.ts","target_before":"class B {\\n"}
3024
+ JSON
2957
3025
  `);
2958
3026
  }
2959
3027
  main(process.argv.slice(2)).then((code) => {
package/docs/commands.md CHANGED
@@ -73,6 +73,10 @@ blockpatch move --src src/foo.ts --src-start $'\nfunction removeMe() {' --src-en
73
73
 
74
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
75
 
76
+ `--target-before` means “this exact context is before the insertion point,” so the payload is inserted after that context. `--target-after` means “this exact context is after the insertion point,” so the payload is inserted before that context. These names describe the anchor's relationship to the insertion point; they do not mean “insert before this text” or “insert after this text.”
77
+
78
+ `--src-start`, `--src-end`, `--target-before`, and `--target-after` are byte-exact and newline-sensitive. `src_start`/`src_end` select bytes from the beginning of `src_start` through the end of the first following `src_end`. `blockpatch` does not add separators, so include the newlines you want moved or inserted. If `target_before` does not end with `\n` and the payload does not start with `\n`, they will be joined directly.
79
+
76
80
  ## Output
77
81
 
78
82
  ```sh
package/docs/spec.md CHANGED
@@ -212,6 +212,8 @@ Request shapes:
212
212
  - `payload` is only valid when `src` is `/dev/null`; it must be non-empty for in-file insertion.
213
213
  - `expected_payload_sha256` is optional.
214
214
 
215
+ `src_start`, `src_end`, `target_before`, and `target_after` are byte-exact and newline-sensitive. Source selection starts at `src_start` and ends after the first following `src_end`; include any leading or trailing newline you want in the selected payload. `target_before` is the exact context before the insertion point, so insertion occurs after it. `target_after` is the exact context after the insertion point, so insertion occurs before it. `blockpatch` never inserts extra newlines or spacing between anchors and payload.
216
+
215
217
  Insertion:
216
218
 
217
219
  ```json
@@ -272,6 +274,14 @@ type ApplyResult = {
272
274
  status: "applied" | "noop" | "already_applied"
273
275
  strip_components?: number
274
276
  patch?: string
277
+ warnings?: Array<{
278
+ code: "adjacent_bytes"
279
+ message: string
280
+ path: string
281
+ phase: "target"
282
+ boundary: "target_before+payload" | "payload+target_after"
283
+ suggested_action: string
284
+ }>
275
285
  moves: Array<{
276
286
  id: string
277
287
  src: string
@@ -289,7 +299,7 @@ type ApplyResult = {
289
299
 
290
300
  `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
301
 
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`.
302
+ `patch` is present when `move --diff --json-output` is used. `warnings` is present when a move validates but may surprise a caller, such as an insertion boundary where neither side contains a newline and the bytes will be joined directly. 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
303
 
294
304
  With `--json-output`, errors print:
295
305
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blockpatch",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Hash-verified, reviewable block-move patches for coding agents",
5
5
  "packageManager": "bun@1.3.14",
6
6
  "type": "module",