@oh-my-pi/hashline 17.2.15 → 17.3.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/src/messages.ts CHANGED
@@ -281,11 +281,10 @@ export function pasteAfterBlockUnresolvedLoweredWarning(line: number): string {
281
281
  return unresolvedLoweredWarning(`PUT >${line}*`, line, `PUT >${line}`);
282
282
  }
283
283
  /**
284
- * A one-sided boundary echo whose payload is too short to be the widened
285
- * range's full content: dropping the echo deletes range line(s) the payload
286
- * never restates (the "widened range" reading), while the "range shifted by
287
- * the echo" reading keeps them. The readings produce different files, so the
288
- * edit is rejected instead of repaired.
284
+ * A one-sided exact boundary echo cannot cover the selected range after the
285
+ * duplicated body rows are removed. Applying or dropping it would lose
286
+ * distinct range content, so the edit is rejected unless a parse-restoring
287
+ * boundary combination proves another reading.
289
288
  */
290
289
  export function ambiguousBoundaryEchoMessage(
291
290
  startLine: number,
@@ -299,82 +298,60 @@ export function ambiguousBoundaryEchoMessage(
299
298
  : `ends by restating the ${count} line(s) just below the range`;
300
299
  return (
301
300
  `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the body ${where}, ` +
302
- `but is too short to be the full final content of the widened range — applying it as-is or ` +
303
- `auto-repairing would delete range line(s) the body never restates. ` +
304
- `Re-issue with the range covering exactly the lines that change and the body as their complete ` +
305
- `final content: drop the restated keeper from the body, or widen the range to consume it.`
301
+ `but is too short to be the full final content of the selected range. ` +
302
+ `Re-issue with the range covering exactly the lines that change and the body as their complete final content.`
306
303
  );
307
304
  }
308
305
 
309
306
  /**
310
- * A replacement range deletes trailing structural closer(s) the payload never
311
- * restates, and nothing anchors the payload inside the block those closers
312
- * terminate: the payload has no unmatched opener for them and its indentation
313
- * is not deeper than the closer. Sparing the closer would have to guess
314
- * whether the payload belongs before it (inside the block) or after it (a
315
- * sibling), so the edit is rejected instead of repaired.
307
+ * A syntax-essential selected edge can be retained on either side of the
308
+ * payload, but indentation does not establish which placement was intended.
316
309
  */
317
- export function ambiguousCloserSpareMessage(
318
- startLine: number,
319
- endLine: number,
320
- closerLine: number,
321
- count: number,
322
- ): string {
323
- const closers = count === 1 ? `line ${closerLine}` : `lines ${closerLine}-${closerLine + count - 1}`;
310
+ export function ambiguousBoundaryPlacementMessage(startLine: number, endLine: number): string {
324
311
  return (
325
- `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the range deletes the closing-delimiter ` +
326
- `${closers} but the body never restates it, and the body claims no position inside that block ` +
327
- `(no unmatched opener, indentation not deeper than the closer) — whether the new content belongs ` +
328
- `before or after the closer is ambiguous. Restate the closer in the body at the intended position, ` +
329
- `or use \`PUT <${closerLine}:\` / \`PUT >${closerLine}:\` instead.`
312
+ `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: a selected boundary row is required for the file to parse, ` +
313
+ `but the body indentation does not establish whether it belongs before or after that row. ` +
314
+ `Re-read the region and re-issue with a range that excludes every unchanged boundary row.`
330
315
  );
331
316
  }
317
+
332
318
  /**
333
- * A replacement range starts by deleting structural closer(s) the payload
334
- * never restates — the "range started one line early, on the `}` that ends
335
- * the construct above" mistake — but the payload's indentation claims a depth
336
- * inside the block those closers terminate, so whether the new content
337
- * belongs before or after the spared closer is ambiguous. Rejected instead of
338
- * repaired; the at-or-above-depth reading is auto-repaired by sparing the
339
- * closer ahead of the payload.
319
+ * Exact-text boundary rows were removed because the remaining payload covers
320
+ * the selected range and the same rows already survive immediately outside it.
340
321
  */
341
- export function ambiguousLeadingCloserSpareMessage(startLine: number, endLine: number, count: number): string {
342
- const closers = count === 1 ? `line ${startLine}` : `lines ${startLine}-${startLine + count - 1}`;
322
+ export function textualBoundaryEchoWarning(startLine: number, leading: number, trailing: number): string {
323
+ const parts: string[] = [];
324
+ if (leading > 0) parts.push(`${leading} leading`);
325
+ if (trailing > 0) parts.push(`${trailing} trailing`);
343
326
  return (
344
- `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the range starts by deleting the closing-delimiter ` +
345
- `${closers} but the body never restates it, and the body's indentation claims a depth inside the block that ` +
346
- `closer terminates — whether the new content belongs before or after the closer is ambiguous. ` +
347
- `Start the range on the first line that actually changes, or restate the closer in the body at the intended position.`
327
+ `Auto-repaired a replacement boundary echo at line ${startLine}: dropped ${parts.join(" and ")} body line(s) ` +
328
+ `already present outside the range. Issue the body as final content for the selected range only.`
348
329
  );
349
330
  }
350
331
 
351
332
  /**
352
- * A replacement range deletes more opening delimiter(s) than the payload
353
- * reopens while the matching closer(s) survive below the range — the
354
- * "payload is a complete construct but the range ends mid-block" mistake.
355
- * Surfaced as a warning, never a rejection: the applier is language-agnostic
356
- * and opener/closer text shape cannot prove a syntactic block (the braces may
357
- * be literal prose), so the edit applies as authored and the author decides.
333
+ * A replacement range's boundary disposition was corrected by the
334
+ * syntax-probe-judged search: syntax-essential source boundary rows were
335
+ * retained, or exact body echoes of surviving outside rows were removed. The
336
+ * authored result did not parse and the selected result does.
358
337
  */
359
- export function midBlockRangeWarning(startLine: number, endLine: number, orphaned: number): string {
338
+ export function boundaryVariantRepairWarning(startLine: number, kept: number, dropped: number): string {
339
+ const keptPart = kept === 0 ? "" : `retained ${kept} syntax-essential source boundary row(s) selected by the range`;
340
+ const droppedPart = dropped === 0 ? "" : `dropped ${dropped} body row(s) duplicated just outside the range`;
341
+ const action = [keptPart, droppedPart].filter(Boolean).join(" and ");
360
342
  return (
361
- `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` deleted ${orphaned} opening delimiter(s) the body never ` +
362
- `reopens. If this file is brace-structured code, the matching closing line(s) below the range are now ` +
363
- `orphaned — the range likely ended mid-block. If the body was the construct's complete new content, ` +
364
- `re-issue with a block op on the construct's opening line (\`PUT N*:\`) so the closing line resolves ` +
365
- `automatically; if the delimiters are literal text, ignore this warning.`
343
+ `Auto-repaired replacement boundaries at line ${startLine}: ${action}. ` +
344
+ `The result was verified by the syntax probe — re-issue with the range covering exactly the changed ` +
345
+ `lines and the body as their complete final content.`
366
346
  );
367
347
  }
368
348
 
369
349
  /**
370
350
  * The applied result no longer parses while the pre-edit content did: the
371
351
  * patch introduced a syntax error. Advisory, never a rejection — the applier
372
- * honors the authored edit — but the breakage is machine-confirmed (the
373
- * tree-sitter probe parsed the original and rejects the result), so it is
374
- * surfaced in the same response instead of waiting for a compiler pass. The
375
- * classic trigger is a balance-neutral misplacement: a statement landed on
376
- * the wrong line number with no delimiter anomaly for the repair heuristics
377
- * to notice.
352
+ * honors the authored edit — but the breakage is machine-confirmed by
353
+ * tree-sitter and surfaced in the same response instead of waiting for a
354
+ * compiler pass.
378
355
  */
379
356
  export function editBrokeParseWarning(firstChangedLine: number | undefined): string {
380
357
  const at = firstChangedLine === undefined ? "" : ` near line ${firstChangedLine}`;
@@ -392,6 +369,10 @@ export function editBrokeParseWarning(firstChangedLine: number | undefined): str
392
369
  export const UNRESOLVED_BLOCK_INTERNAL =
393
370
  "internal error: unresolved block edit reached the applier (resolveBlockEdits was not run).";
394
371
 
372
+ /** Internal invariant: clipboard edits must be concrete before application. */
373
+ export const UNRESOLVED_CLIPBOARD_INTERNAL =
374
+ "internal error: unresolved clipboard edit reached the applier (resolveClipboardEdits was not run).";
375
+
395
376
  /** `REM` received a body row or coexists with line edits. */
396
377
  export const REM_TAKES_NO_BODY =
397
378
  "`REM` deletes the whole file and takes no body rows or line ops. Issue it alone under the header.";
package/src/prompt.md CHANGED
@@ -7,9 +7,11 @@ Section: `[PATH#TAG]`; `TAG`: 4-hex snapshot from latest `read`/`search`, REQUIR
7
7
  <ops>
8
8
  `PUT N.=M:`: replace original inclusive lines N–M with body.
9
9
  `PUT N*:`: replace syntactic block beginning N; closing line resolved.
10
- `PUT <N:` / `PUT >N:`: insert before/after N; `<1` head, `>$` tail.
10
+ `PUT <N:` insert body rows before line N (`PUT <1:` = file head).
11
+ `PUT >N:` insert body rows after line N (`PUT >$:` = file tail).
11
12
  `PUT >N*:`: insert after block N's end, at sibling depth. Append inside block: `PUT >M:`.
12
- `PUT <N` / `PUT >N` / `PUT N.=M @name` / `PUT N* @name`: paste captured register at gap/range/resolved block; no `:` or body. Unlabeled gap paste: anonymous register; range/block paste: `@name` required.
13
+ `PUT <N @name` / `PUT >N @name` paste register `@name` at the gap before/after line N; omit `@name` for the anonymous register.
14
+ `PUT N.=M @name` / `PUT N* @name` paste `@name` over the range / resolved block; `@name` required here.
13
15
  `CUT N.=M` / `CUT N*`: delete and capture inclusive lines N–M / block N; anonymous or given `@name`.
14
16
  `REM`: delete section file. `MV DEST`: move/rename (quote paths with spaces); prior edits apply to source, final content to `DEST`.
15
17
  Single line: `PUT N.=N:` / `CUT N.=N`. Ranges name original inclusive touched lines; body length irrelevant.
package/src/syntax.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * Syntax probe for candidate edit results, via the native tree-sitter parser.
3
3
  *
4
- * Delimiter-balance arithmetic cannot tell a block closer from a `}` inside a
5
- * regex literal, a string, or Markdown prose. A parser can, so it holds veto
6
- * power over every repair whose justification is "this line closes a syntactic
7
- * block": when the edit the author actually wrote still parses, no such repair
8
- * may rewrite it. The probe never *forces* a repair — an unrecognized language
9
- * or an already-broken file simply yields no veto, leaving the delimiter
10
- * heuristics as the only available evidence.
4
+ * Replacement-boundary repair uses parsing as its semantic filter: exact
5
+ * outside-row equality may justify dropping a duplicated payload edge, while
6
+ * retaining a selected source boundary additionally requires source-range
7
+ * structure, indentation, or a narrow pure-closer shape. An unrecognized
8
+ * language yields no structural proof, so only evidence-complete textual
9
+ * normalization remains available.
11
10
  */
12
11
 
13
12
  import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
@@ -16,6 +15,33 @@ import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
16
15
  const parseCache = new Map<string, boolean>();
17
16
  const PARSE_CACHE_MAX = 256;
18
17
 
18
+ const boundaryCache = new Map<string, readonly number[]>();
19
+
20
+ /** Syntactic node boundaries outside a visible source range. */
21
+ export function enclosingBoundaries(
22
+ lines: readonly string[],
23
+ path: string,
24
+ startLine: number,
25
+ endLine: number,
26
+ ): readonly number[] {
27
+ const text = lines.join("\n");
28
+ const key = `${Bun.hash(text).toString(36)}:${text.length}:${path}:${startLine}:${endLine}`;
29
+ const cached = boundaryCache.get(key);
30
+ if (cached !== undefined) return cached;
31
+ let boundaries: readonly number[];
32
+ try {
33
+ boundaries = enclosingBlockBoundaries({ code: text, path, ranges: [{ startLine, endLine }] }) ?? [];
34
+ } catch {
35
+ boundaries = [];
36
+ }
37
+ if (boundaryCache.size >= PARSE_CACHE_MAX) {
38
+ const oldest = boundaryCache.keys().next().value;
39
+ if (oldest !== undefined) boundaryCache.delete(oldest);
40
+ }
41
+ boundaryCache.set(key, boundaries);
42
+ return boundaries;
43
+ }
44
+
19
45
  /**
20
46
  * `true` when `text` parses without a syntax error under the language inferred
21
47
  * from `path`. `false` covers "does not parse" and "cannot tell" alike — no