@oh-my-pi/hashline 17.1.1 → 17.1.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.2] - 2026-07-24
6
+
7
+ ### Changed
8
+
9
+ - Bare `- ` bullet body rows are now auto-accepted as literal content with a warning when the hunk is unambiguously a Markdown bullet list (every `-` row bullet-shaped and no plain `+new` diff counterpart); ambiguous `-` rows still fail with the teaching error.
10
+
5
11
  ## [17.0.8] - 2026-07-22
6
12
 
7
13
  ### Changed
@@ -20,6 +20,12 @@ export declare const ABORT_MARKER = "*** Abort";
20
20
  export declare const REPLACE_PAIR_COALESCED_WARNING = "Two hunks targeted the same range; kept only the second. One `SWAP N.=M:` hunk per range \u2014 the body is the final content, never old+new.";
21
21
  /** Bare body rows auto-converted to literal `+` rows. */
22
22
  export declare const BARE_BODY_AUTO_PIPED_WARNING = "Auto-prefixed bare body row(s) with `+`. Body rows must be `+TEXT` literal lines.";
23
+ /**
24
+ * Bare `-` body rows accepted as literal Markdown bullets. Only emitted when
25
+ * the hunk is unambiguously a bullet list: every `-` row is bullet-shaped
26
+ * (`- item`) and the body has no unified-diff `+new` counterpart rows.
27
+ */
28
+ export declare const MINUS_BULLET_AUTO_PIPED_WARNING = "Auto-prefixed bare `- ` bullet row(s) as literal content. `-` rows never remove lines \u2014 the range does that; always prefix literal body rows with `+`: `+- item`.";
23
29
  /** Unified-diff-style `-` row in a hunk body. */
24
30
  export declare const MINUS_ROW_REJECTED = "`-` rows are not valid; the range already names the lines being changed. For Markdown bullets or other literal `-` lines, prefix the literal row with `+`: `+- item`.";
25
31
  /** Replace hunk with no body. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/hashline",
4
- "version": "17.1.1",
4
+ "version": "17.1.2",
5
5
  "description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -33,7 +33,7 @@
33
33
  "fmt": "biome format --write ."
34
34
  },
35
35
  "dependencies": {
36
- "@oh-my-pi/pi-natives": "17.1.1",
36
+ "@oh-my-pi/pi-natives": "17.1.2",
37
37
  "lru-cache": "11.5.2"
38
38
  },
39
39
  "devDependencies": {
package/src/messages.ts CHANGED
@@ -52,6 +52,14 @@ export const REPLACE_PAIR_COALESCED_WARNING = `Two hunks targeted the same range
52
52
  export const BARE_BODY_AUTO_PIPED_WARNING =
53
53
  "Auto-prefixed bare body row(s) with `+`. Body rows must be `+TEXT` literal lines.";
54
54
 
55
+ /**
56
+ * Bare `-` body rows accepted as literal Markdown bullets. Only emitted when
57
+ * the hunk is unambiguously a bullet list: every `-` row is bullet-shaped
58
+ * (`- item`) and the body has no unified-diff `+new` counterpart rows.
59
+ */
60
+ export const MINUS_BULLET_AUTO_PIPED_WARNING =
61
+ "Auto-prefixed bare `- ` bullet row(s) as literal content. `-` rows never remove lines — the range does that; always prefix literal body rows with `+`: `+- item`.";
62
+
55
63
  /** Unified-diff-style `-` row in a hunk body. */
56
64
  export const MINUS_ROW_REJECTED =
57
65
  "`-` rows are not valid; the range already names the lines being changed. For Markdown bullets or other literal `-` lines, prefix the literal row with `+`: `+- item`.";
package/src/parser.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  DELETE_TAKES_NO_BODY,
11
11
  EMPTY_BLOCK,
12
12
  EMPTY_INSERT,
13
+ MINUS_BULLET_AUTO_PIPED_WARNING,
13
14
  MINUS_ROW_REJECTED,
14
15
  MOVE_TAKES_NO_BODY,
15
16
  REM_TAKES_NO_BODY,
@@ -43,6 +44,13 @@ function isSkippableCommentLine(line: string): boolean {
43
44
  */
44
45
  const BARE_LITERAL_VALUE_RE = /^\s*(?:"[^"]*"|'[^']*'|[-+]?\d+(?:\.\d+)?)\s*,?\s*$/;
45
46
 
47
+ /**
48
+ * Markdown-bullet shape: optional indent, `-`, exactly one space, then
49
+ * content. Unified-diff `-` rows almost never match — code lines get the `-`
50
+ * glued on (`-old()`) and indented deletions carry multiple spaces (`- x`).
51
+ */
52
+ const MD_BULLET_ROW_RE = /^\s*- \S/;
53
+
46
54
  function detectApplyPatchContamination(text: string, _hasPending: boolean): string | null {
47
55
  const trimmed = text.trimStart();
48
56
  if (trimmed.length === 0) return null;
@@ -93,7 +101,7 @@ interface PendingComment {
93
101
  text: string;
94
102
  }
95
103
 
96
- type PayloadRow = { kind: "literal"; text: string; lineNum: number; bare?: boolean };
104
+ type PayloadRow = { kind: "literal"; text: string; lineNum: number; bare?: boolean; minus?: boolean };
97
105
 
98
106
  interface Pending {
99
107
  target: BlockTarget;
@@ -284,8 +292,12 @@ export class Executor {
284
292
  if (this.#pending.target.kind === "delete") throw new Error(`line ${lineNum}: ${DELETE_TAKES_NO_BODY}`);
285
293
  if (this.#pending.target.kind === "delete_block")
286
294
  throw new Error(`line ${lineNum}: ${DELETE_BLOCK_TAKES_NO_BODY}`);
287
- if (text.trimStart().charCodeAt(0) === 45 /* - */) throw new Error(`line ${lineNum}: ${MINUS_ROW_REJECTED}`);
288
- if (!this.#warnings.includes(BARE_BODY_AUTO_PIPED_WARNING)) this.#warnings.push(BARE_BODY_AUTO_PIPED_WARNING);
295
+ const row: PayloadRow = { kind: "literal", text, lineNum, bare: true };
296
+ // `-` rows are held and judged at flush time by #resolveMinusRows,
297
+ // once the whole body is visible.
298
+ if (text.trimStart().charCodeAt(0) === 45 /* - */) row.minus = true;
299
+ else if (!this.#warnings.includes(BARE_BODY_AUTO_PIPED_WARNING))
300
+ this.#warnings.push(BARE_BODY_AUTO_PIPED_WARNING);
289
301
  this.#commitDeferredBlanks(this.#pending);
290
302
  // Defer read-output line-number stripping to #flushPending: a bare
291
303
  // "N:text" row is only a copy-paste artifact from snapshot output
@@ -294,7 +306,7 @@ export class Executor {
294
306
  // with "digits:" (YAML ports "42:hello", timestamps "12:30") when it
295
307
  // sits next to an unprefixed sibling. Rows with an explicit "+" go
296
308
  // through #handleLiteralPayload and are never bare, never stripped.
297
- this.#pending.payloads.push({ kind: "literal", text, lineNum, bare: true });
309
+ this.#pending.payloads.push(row);
298
310
  return;
299
311
  }
300
312
  if (text.trim().length === 0) return;
@@ -326,6 +338,38 @@ export class Executor {
326
338
  pending.deferredBlanks = [];
327
339
  }
328
340
 
341
+ /**
342
+ * Judge bare `-` body rows once the whole hunk body is known. They are
343
+ * usually unified-diff contamination (`-old` next to `+new`) and inserting
344
+ * them would corrupt the file, so they are rejected — EXCEPT when the body
345
+ * is unambiguously a Markdown bullet list: every `-` row is bullet-shaped
346
+ * (`- item`) and the body is either fully bare or already contains an
347
+ * explicit `+- item` sibling. Those rows are kept as literal content with a
348
+ * warning instead of failing the patch.
349
+ */
350
+ #resolveMinusRows(payloads: readonly PayloadRow[]): void {
351
+ let firstMinus: PayloadRow | undefined;
352
+ let allBulletShaped = true;
353
+ let hasExplicit = false;
354
+ let hasExplicitBullet = false;
355
+ for (const row of payloads) {
356
+ if (row.minus) {
357
+ firstMinus ??= row;
358
+ allBulletShaped &&= MD_BULLET_ROW_RE.test(row.text);
359
+ } else if (!row.bare) {
360
+ hasExplicit = true;
361
+ hasExplicitBullet ||= MD_BULLET_ROW_RE.test(row.text);
362
+ }
363
+ }
364
+ if (firstMinus === undefined) return;
365
+ if (allBulletShaped && (!hasExplicit || hasExplicitBullet)) {
366
+ if (!this.#warnings.includes(MINUS_BULLET_AUTO_PIPED_WARNING))
367
+ this.#warnings.push(MINUS_BULLET_AUTO_PIPED_WARNING);
368
+ return;
369
+ }
370
+ throw new Error(`line ${firstMinus.lineNum}: ${MINUS_ROW_REJECTED}`);
371
+ }
372
+
329
373
  /**
330
374
  * Strip a single read-output line-number prefix (`N:`) from every bare body
331
375
  * row, but only when *all* bare rows carry one. A uniform set of prefixes is
@@ -388,6 +432,7 @@ export class Executor {
388
432
  const pending = this.#pending;
389
433
  if (!pending) return;
390
434
  const { target, lineNum, payloads } = pending;
435
+ this.#resolveMinusRows(payloads);
391
436
  this.#stripBarePrefixesIfUniform(payloads);
392
437
  this.#pending = undefined;
393
438
  if (target.kind === "delete") {