pi-hashline-edit-pro 4.2.6 → 4.2.7

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/grep.ts CHANGED
@@ -12,7 +12,7 @@ import { toCwd } from "./paths";
12
12
  import { loadP, loadGuide } from "./prompts";
13
13
  import { normReq } from "./payload-contract";
14
14
  import { abortIf, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
15
- import { markServed as markServedScoped } from "./anchor-registry";
15
+ import { markServed as markServedScoped, withAnchorSession } from "./anchor-registry";
16
16
  import { buildServedMap } from "./served";
17
17
  import { Text } from "@earendil-works/pi-tui";
18
18
  import { expandHint, getResultText, reuseText, type CallT, type FgT } from "./replace-render";
@@ -553,172 +553,174 @@ export function regGrep(pi: ExtensionAPI): void {
553
553
  },
554
554
 
555
555
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
556
- const canonical = normReq(params);
557
- assertGrepReq(canonical);
558
- const req = canonical;
559
- const context = req.context ?? 0;
560
- const limit = req.limit ?? 100;
561
- const base = req.path ? toCwd(req.path, ctx.cwd) : ctx.cwd;
562
- abortIf(signal);
563
- let baseStat;
564
- try {
565
- baseStat = await stat(base);
566
- } catch (error) {
567
- if (errCode(error) === "ENOENT") {
568
- throw new Error(`[E_NOT_FOUND] File not found: ${req.path ?? ctx.cwd}`);
569
- }
570
- throw new Error(`[E_ACCESS] Cannot access path: ${req.path ?? ctx.cwd}`);
571
- }
572
- const globRoot = baseStat.isFile() ? dirname(base) : base;
573
- const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
574
- const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
575
- const rgPath = await resolveRgPath();
576
- const hits: FileHit[] = [];
577
- let matches = 0;
578
- let limitTruncated = false;
579
- let rowTruncated = false;
580
- let rowCount = 0;
581
- let byteCount = 0;
582
- let totalRows = 0;
583
- let totalBytes = 0;
584
- let truncatedBy: "lines" | "bytes" | null = null;
585
- let linesReplaced = 0;
586
- let countOnly = false;
587
- let poolSkipped = 0;
588
- const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
556
+ return withAnchorSession(ctx, async () => {
557
+ const canonical = normReq(params);
558
+ assertGrepReq(canonical);
559
+ const req = canonical;
560
+ const context = req.context ?? 0;
561
+ const limit = req.limit ?? 100;
562
+ const base = req.path ? toCwd(req.path, ctx.cwd) : ctx.cwd;
563
+ abortIf(signal);
564
+ let baseStat;
589
565
  try {
590
- return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation, signal });
566
+ baseStat = await stat(base);
591
567
  } catch (error) {
592
- if (!isPoolExhaustedError(error)) throw error;
593
- poolSkipped += 1;
594
- return undefined;
568
+ if (errCode(error) === "ENOENT") {
569
+ throw new Error(`[E_NOT_FOUND] File not found: ${req.path ?? ctx.cwd}`);
570
+ }
571
+ throw new Error(`[E_ACCESS] Cannot access path: ${req.path ?? ctx.cwd}`);
595
572
  }
596
- };
597
- const readGrepFile = makeGrepReader("real");
598
- const readGrepFileShadow = makeGrepReader("shadow");
599
- const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
600
- const sortedFiles = [...rgMatches.keys()].sort(cmp);
601
- for (let f = 0; f < sortedFiles.length; f++) {
602
- abortIf(signal);
603
- const absPath = sortedFiles[f]!;
604
- const allNums = rgMatches.get(absPath) ?? [];
605
- const totalForFile = allNums.length;
606
- const sortedNums = [...allNums].sort((a, b) => a - b);
607
- const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
608
- if (countOnly) {
573
+ const globRoot = baseStat.isFile() ? dirname(base) : base;
574
+ const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
575
+ const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
576
+ const rgPath = await resolveRgPath();
577
+ const hits: FileHit[] = [];
578
+ let matches = 0;
579
+ let limitTruncated = false;
580
+ let rowTruncated = false;
581
+ let rowCount = 0;
582
+ let byteCount = 0;
583
+ let totalRows = 0;
584
+ let totalBytes = 0;
585
+ let truncatedBy: "lines" | "bytes" | null = null;
586
+ let linesReplaced = 0;
587
+ let countOnly = false;
588
+ let poolSkipped = 0;
589
+ const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
590
+ try {
591
+ return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation, signal });
592
+ } catch (error) {
593
+ if (!isPoolExhaustedError(error)) throw error;
594
+ poolSkipped += 1;
595
+ return undefined;
596
+ }
597
+ };
598
+ const readGrepFile = makeGrepReader("real");
599
+ const readGrepFileShadow = makeGrepReader("shadow");
600
+ const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
601
+ const sortedFiles = [...rgMatches.keys()].sort(cmp);
602
+ for (let f = 0; f < sortedFiles.length; f++) {
603
+ abortIf(signal);
604
+ const absPath = sortedFiles[f]!;
605
+ const allNums = rgMatches.get(absPath) ?? [];
606
+ const totalForFile = allNums.length;
607
+ const sortedNums = [...allNums].sort((a, b) => a - b);
608
+ const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
609
+ if (countOnly) {
610
+ if (globRegex) {
611
+ const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
612
+ const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
613
+ if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
614
+ }
615
+ const norm = await readGrepFileShadow(absPath);
616
+ if (!norm) continue;
617
+ const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
618
+ const display = displayRowsForHit(hit);
619
+ totalRows += display.length;
620
+ for (const r of display) totalBytes += Buffer.byteLength(r, "utf-8") + 1;
621
+ const remainingCountOnly = limit - matches;
622
+ if (remainingCountOnly > 0) {
623
+ const add = Math.min(hit.matchCount, remainingCountOnly);
624
+ matches += add;
625
+ if (hit.matchCount > remainingCountOnly) limitTruncated = true;
626
+ } else {
627
+ limitTruncated = true;
628
+ }
629
+ continue;
630
+ }
631
+ const remaining = limit - matches;
632
+ if (remaining <= 0) {
633
+ limitTruncated = true;
634
+ break;
635
+ }
609
636
  if (globRegex) {
610
637
  const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
611
638
  const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
612
639
  if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
613
640
  }
614
- const norm = await readGrepFileShadow(absPath);
641
+ const norm = await readGrepFile(absPath);
615
642
  if (!norm) continue;
616
- const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
643
+ const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
644
+ if (!hit) continue;
617
645
  const display = displayRowsForHit(hit);
618
- totalRows += display.length;
619
- for (const r of display) totalBytes += Buffer.byteLength(r, "utf-8") + 1;
620
- const remainingCountOnly = limit - matches;
621
- if (remainingCountOnly > 0) {
622
- const add = Math.min(hit.matchCount, remainingCountOnly);
623
- matches += add;
624
- if (hit.matchCount > remainingCountOnly) limitTruncated = true;
625
- } else {
626
- limitTruncated = true;
646
+ const keptRows: string[] = [];
647
+ const keptHashes: string[] = [];
648
+ const keptLineNumbers: number[] = [];
649
+ const keptFragmented: boolean[] = [];
650
+ for (let i = 0; i < display.length; i++) {
651
+ const row = display[i]!;
652
+ const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
653
+ if (rowCount >= DEFAULT_MAX_LINES || byteCount + rowBytes > DEFAULT_MAX_BYTES) {
654
+ rowTruncated = true;
655
+ if (truncatedBy === null) truncatedBy = byteCount + rowBytes > DEFAULT_MAX_BYTES ? "bytes" : "lines";
656
+ for (let j = i; j < display.length; j++) {
657
+ totalRows += 1;
658
+ totalBytes += Buffer.byteLength(display[j]!, "utf-8") + 1;
659
+ }
660
+ break;
661
+ }
662
+ keptRows.push(row);
663
+ keptHashes.push(hit.hashes[i]!);
664
+ keptLineNumbers.push(hit.lineNumbers[i]!);
665
+ keptFragmented.push(hit.fragmented[i]!);
666
+ if (hit.fragmented[i]) linesReplaced += 1;
667
+ rowCount += 1;
668
+ byteCount += rowBytes;
669
+ totalRows += 1;
670
+ totalBytes += rowBytes;
627
671
  }
628
- continue;
629
- }
630
- const remaining = limit - matches;
631
- if (remaining <= 0) {
632
- limitTruncated = true;
633
- break;
672
+ if (hit.totalMatchCount > hit.matchCount) limitTruncated = true;
673
+ matches += hit.matchCount;
674
+ const displayHit: FileHit = { ...hit, rows: keptRows, hashes: keptHashes, lineNumbers: keptLineNumbers, fragmented: keptFragmented };
675
+ hits.push(displayHit);
676
+ if (rowTruncated) countOnly = true;
634
677
  }
635
- if (globRegex) {
636
- const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
637
- const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
638
- if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
678
+ hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
679
+ for (const hit of hits) {
680
+ markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
639
681
  }
640
- const norm = await readGrepFile(absPath);
641
- if (!norm) continue;
642
- const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
643
- if (!hit) continue;
644
- const display = displayRowsForHit(hit);
645
- const keptRows: string[] = [];
646
- const keptHashes: string[] = [];
647
- const keptLineNumbers: number[] = [];
648
- const keptFragmented: boolean[] = [];
649
- for (let i = 0; i < display.length; i++) {
650
- const row = display[i]!;
651
- const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
652
- if (rowCount >= DEFAULT_MAX_LINES || byteCount + rowBytes > DEFAULT_MAX_BYTES) {
653
- rowTruncated = true;
654
- if (truncatedBy === null) truncatedBy = byteCount + rowBytes > DEFAULT_MAX_BYTES ? "bytes" : "lines";
655
- for (let j = i; j < display.length; j++) {
656
- totalRows += 1;
657
- totalBytes += Buffer.byteLength(display[j]!, "utf-8") + 1;
682
+ const blocks = hits
683
+ .map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
684
+ .join("\n");
685
+ const notes: string[] = [];
686
+ if (rowTruncated) notes.push(`[grep: output truncated at ${DEFAULT_MAX_LINES} rows or ${formatSize(DEFAULT_MAX_BYTES)}; refine the pattern to see more.]`);
687
+ if (limitTruncated) notes.push(`[grep: showing first ${limit} matches; increase limit to see more.]`);
688
+ if (linesReplaced > 0) notes.push(`[grep: ${linesReplaced} line(s) exceed ${formatSize(MAX_GREP_LINE_BYTES)} and are shown as truncated fragments; use read to see the full lines.]`);
689
+ if (poolSkipped > 0) notes.push(`[grep: ${poolSkipped} file(s) skipped because the session's anchor pool is exhausted; narrow the search.]`);
690
+ const truncated = limitTruncated || rowTruncated;
691
+ const truncation: TruncationResult | undefined = rowTruncated
692
+ ? {
693
+ content: blocks,
694
+ truncated: true,
695
+ truncatedBy,
696
+ totalLines: totalRows,
697
+ totalBytes,
698
+ outputLines: rowCount,
699
+ outputBytes: byteCount,
700
+ lastLinePartial: false,
701
+ firstLineExceedsLimit: false,
702
+ maxLines: DEFAULT_MAX_LINES,
703
+ maxBytes: DEFAULT_MAX_BYTES,
658
704
  }
659
- break;
660
- }
661
- keptRows.push(row);
662
- keptHashes.push(hit.hashes[i]!);
663
- keptLineNumbers.push(hit.lineNumbers[i]!);
664
- keptFragmented.push(hit.fragmented[i]!);
665
- if (hit.fragmented[i]) linesReplaced += 1;
666
- rowCount += 1;
667
- byteCount += rowBytes;
668
- totalRows += 1;
669
- totalBytes += rowBytes;
670
- }
671
- if (hit.totalMatchCount > hit.matchCount) limitTruncated = true;
672
- matches += hit.matchCount;
673
- const displayHit: FileHit = { ...hit, rows: keptRows, hashes: keptHashes, lineNumbers: keptLineNumbers, fragmented: keptFragmented };
674
- hits.push(displayHit);
675
- if (rowTruncated) countOnly = true;
676
- }
677
- hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
678
- for (const hit of hits) {
679
- markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
680
- }
681
- const blocks = hits
682
- .map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
683
- .join("\n");
684
- const notes: string[] = [];
685
- if (rowTruncated) notes.push(`[grep: output truncated at ${DEFAULT_MAX_LINES} rows or ${formatSize(DEFAULT_MAX_BYTES)}; refine the pattern to see more.]`);
686
- if (limitTruncated) notes.push(`[grep: showing first ${limit} matches; increase limit to see more.]`);
687
- if (linesReplaced > 0) notes.push(`[grep: ${linesReplaced} line(s) exceed ${formatSize(MAX_GREP_LINE_BYTES)} and are shown as truncated fragments; use read to see the full lines.]`);
688
- if (poolSkipped > 0) notes.push(`[grep: ${poolSkipped} file(s) skipped because the session's anchor pool is exhausted; narrow the search.]`);
689
- const truncated = limitTruncated || rowTruncated;
690
- const truncation: TruncationResult | undefined = rowTruncated
691
- ? {
692
- content: blocks,
693
- truncated: true,
694
- truncatedBy,
695
- totalLines: totalRows,
696
- totalBytes,
697
- outputLines: rowCount,
698
- outputBytes: byteCount,
699
- lastLinePartial: false,
700
- firstLineExceedsLimit: false,
701
- maxLines: DEFAULT_MAX_LINES,
702
- maxBytes: DEFAULT_MAX_BYTES,
703
- }
704
- : undefined;
705
- const text = blocks.length > 0
706
- ? `${blocks}${notes.length > 0 ? `\n${notes.join("\n")}` : ""}`
707
- : notes.length > 0
708
- ? `No matches found.\n${notes.join("\n")}`
709
- : "No matches found.";
710
- return {
711
- content: [{ type: "text", text }],
712
- details: {
713
- ...(truncation ? { truncation } : {}),
714
- ...(linesReplaced > 0 ? { linesTruncated: true as const } : {}),
715
- metrics: {
716
- matches,
717
- files: hits.length,
718
- truncated,
705
+ : undefined;
706
+ const text = blocks.length > 0
707
+ ? `${blocks}${notes.length > 0 ? `\n${notes.join("\n")}` : ""}`
708
+ : notes.length > 0
709
+ ? `No matches found.\n${notes.join("\n")}`
710
+ : "No matches found.";
711
+ return {
712
+ content: [{ type: "text", text }],
713
+ details: {
714
+ ...(truncation ? { truncation } : {}),
715
+ ...(linesReplaced > 0 ? { linesTruncated: true as const } : {}),
716
+ metrics: {
717
+ matches,
718
+ files: hits.length,
719
+ truncated,
720
+ },
719
721
  },
720
- },
721
- };
722
+ };
723
+ });
722
724
  },
723
725
  });
724
726
  }
@@ -1,4 +1,4 @@
1
- import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray } from "../utils";
1
+ import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray, assertNoNul } from "../utils";
2
2
  import { parseHashRef, parseText, type Anchor } from "./parse";
3
3
  import { HASH_SEP, stripRowPrefix, canon } from "./hash";
4
4
  import { HASH_RUN } from "./alphabet";
@@ -172,6 +172,7 @@ export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
172
172
  assertItem(edit as Record<string, unknown>);
173
173
 
174
174
  const replaceLines = parseText(decodeStringArray(edit.replacement_lines, warnings) ?? edit.replacement_lines, warnings);
175
+ assertNoNul(replaceLines);
175
176
  const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
176
177
  return stripAnchorRow(ref.trim(), "remove_from/remove_to entry", warnings);
177
178
  }) as [string, string];
package/src/insert.ts CHANGED
@@ -7,6 +7,7 @@ import { batchMemberFor, ensureBatchBase, executeBatchMember, noteBatchFailure,
7
7
  import { readNormFile, type NormFile } from "./file-reader";
8
8
  import { MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, type Anchor, type HEdit } from "./hashline";
9
9
  import { stripAnchorRow } from "./hashline/resolve";
10
+ import { withAnchorSession } from "./anchor-registry";
10
11
  import { loadP, loadGuide } from "./prompts";
11
12
  import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
12
13
  import { decodeStringArray, isRec, splitLines } from "./utils";
@@ -172,84 +173,86 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
172
173
  renderCall: editRenderCallWrapper(insertPreview, getInsertInput, "insert"),
173
174
  renderResult: editRenderResultWrapper,
174
175
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
175
- const canonical = normReq(params);
176
- const insertWarnings: string[] = [];
177
- if (isRec(canonical)) {
178
- const expanded = decodeStringArray(canonical.lines, insertWarnings, "lines");
179
- if (expanded) canonical.lines = expanded;
180
- }
181
- assertInsertReq(canonical);
182
- const req = canonical;
183
- const targetPath = await resolveEditTargetWithRequirement({
184
- anchor: req.anchor,
185
- providedPath: req.path,
186
- cwd: ctx.cwd,
187
- }).catch((error: unknown) => {
188
- const member = batchMemberFor(_toolCallId);
189
- if (member) noteBatchFailure(member, error);
190
- else suffixPoisonCause(_toolCallId, error);
191
- throw error;
192
- });
193
- let ref: Anchor;
194
- let anchorWarnings: string[];
195
- try {
196
- ({ ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor));
197
- await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
198
- } catch (error) {
199
- const member = batchMemberFor(_toolCallId);
200
- if (member) noteBatchFailure(member, error);
201
- throw error;
202
- }
203
- return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
204
- const member = batchMemberFor(_toolCallId);
205
- if (member) {
206
- const base = await ensureBatchBase({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
207
- const basePreload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
208
- const built = buildInsertEdit(req, basePreload, ref, targetPath);
209
- let hedit: HEdit;
210
- const resWarnings: string[] = [];
211
- try {
212
- hedit = resEdit(built.editParams, resWarnings);
213
- } catch (error) {
214
- noteBatchFailure(member, error);
215
- throw error;
176
+ return withAnchorSession(ctx, async () => {
177
+ const canonical = normReq(params);
178
+ const insertWarnings: string[] = [];
179
+ if (isRec(canonical)) {
180
+ const expanded = decodeStringArray(canonical.lines, insertWarnings, "lines");
181
+ if (expanded) canonical.lines = expanded;
182
+ }
183
+ assertInsertReq(canonical);
184
+ const req = canonical;
185
+ const targetPath = await resolveEditTargetWithRequirement({
186
+ anchor: req.anchor,
187
+ providedPath: req.path,
188
+ cwd: ctx.cwd,
189
+ }).catch((error: unknown) => {
190
+ const member = batchMemberFor(_toolCallId);
191
+ if (member) noteBatchFailure(member, error);
192
+ else suffixPoisonCause(_toolCallId, error);
193
+ throw error;
194
+ });
195
+ let ref: Anchor;
196
+ let anchorWarnings: string[];
197
+ try {
198
+ ({ ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor));
199
+ await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
200
+ } catch (error) {
201
+ const member = batchMemberFor(_toolCallId);
202
+ if (member) noteBatchFailure(member, error);
203
+ throw error;
204
+ }
205
+ return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
206
+ const member = batchMemberFor(_toolCallId);
207
+ if (member) {
208
+ const base = await ensureBatchBase({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
209
+ const basePreload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
210
+ const built = buildInsertEdit(req, basePreload, ref, targetPath);
211
+ let hedit: HEdit;
212
+ const resWarnings: string[] = [];
213
+ try {
214
+ hedit = resEdit(built.editParams, resWarnings);
215
+ } catch (error) {
216
+ noteBatchFailure(member, error);
217
+ throw error;
218
+ }
219
+ return executeBatchMember({
220
+ kind: "insert",
221
+ member,
222
+ targetPath,
223
+ mutationTargetPath,
224
+ cwd: ctx.cwd,
225
+ signal,
226
+ hedit,
227
+ extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
228
+ skipBoundaryDedup: true,
229
+ strictBoundaryDedup: false,
230
+ foldedLines: built.anchorLine === undefined ? 0 : 1,
231
+ });
216
232
  }
217
- return executeBatchMember({
218
- kind: "insert",
219
- member,
220
- targetPath,
221
- mutationTargetPath,
222
- cwd: ctx.cwd,
233
+ const preload = await readNormFile(targetPath, ctx.cwd, {
234
+ signal,
235
+ accessMode: constants.R_OK | constants.W_OK,
236
+ maxLines: MAX_HASH_LINES,
237
+ });
238
+ const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
239
+ const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
240
+ accessMode: constants.R_OK | constants.W_OK,
223
241
  signal,
224
- hedit,
225
- extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
242
+ preloadedNorm: preload,
226
243
  skipBoundaryDedup: true,
227
- strictBoundaryDedup: false,
228
- foldedLines: built.anchorLine === undefined ? 0 : 1,
229
244
  });
230
- }
231
- const preload = await readNormFile(targetPath, ctx.cwd, {
232
- signal,
233
- accessMode: constants.R_OK | constants.W_OK,
234
- maxLines: MAX_HASH_LINES,
235
- });
236
- const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
237
- const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
238
- accessMode: constants.R_OK | constants.W_OK,
239
- signal,
240
- preloadedNorm: preload,
241
- skipBoundaryDedup: true,
242
- });
243
- return commitEdit(pipe, {
244
- path: pipe.path,
245
- absolutePath,
246
- mutationTargetPath,
247
- signal,
248
- verb: "inserted",
249
- noopNoun: "Insertion",
250
- foldedAnchorLines: anchorLine === undefined ? 0 : 1,
251
- prefixWarnings: [...anchorWarnings, ...insertWarnings],
252
- onApplied: () => clearBoundaryBypass(mutationTargetPath),
245
+ return commitEdit(pipe, {
246
+ path: pipe.path,
247
+ absolutePath,
248
+ mutationTargetPath,
249
+ signal,
250
+ verb: "inserted",
251
+ noopNoun: "Insertion",
252
+ foldedAnchorLines: anchorLine === undefined ? 0 : 1,
253
+ prefixWarnings: [...anchorWarnings, ...insertWarnings],
254
+ onApplied: () => clearBoundaryBypass(mutationTargetPath),
255
+ });
253
256
  });
254
257
  });
255
258
  },
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields } from "./utils";
2
+ import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields, assertNoNul } from "./utils";
3
3
 
4
4
  const replacementLinesSchema = Type.Array(
5
5
  Type.String({
@@ -74,6 +74,7 @@ export function assertReq(request: unknown): asserts request is ReqParams {
74
74
  '[E_BAD_SHAPE] Edit request requires "remove_from", "remove_to", and "replacement_lines" (array of strings, one per line; use [] to delete).',
75
75
  );
76
76
  }
77
+ assertNoNul(request.replacement_lines);
77
78
  }
78
79
 
79
80
  export function normReq(input: unknown): unknown {
@@ -136,4 +137,5 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
136
137
  if (!Array.isArray(request.lines) || request.lines.some((line) => typeof line !== "string")) {
137
138
  throw new Error('[E_BAD_SHAPE] Insert request requires "lines" as an array of strings, one element per line.');
138
139
  }
140
+ assertNoNul(request.lines);
139
141
  }