pi-hashline-edit-pro 4.2.10 → 4.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.
@@ -1,6 +1,7 @@
1
1
  import { Markdown, Text } from "@earendil-works/pi-tui";
2
2
  import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
3
3
  import type { ReplaceDetails } from "./replace";
4
+ import { abortedBatchMessageFor } from "./batch";
4
5
  import { withLineNumbers } from "./utils";
5
6
  import { isDedupRow } from "./replace-response";
6
7
  import { getPreviewInput } from "./payload-contract";
@@ -66,6 +67,21 @@ export function fmtResult(diff: string, theme: FgT): string {
66
67
  return colorLines(diff.split("\n"), theme).join("\n");
67
68
  }
68
69
 
70
+ export function highlightBatchRefs(text: string, theme: FgT): string {
71
+ const pattern = /\b([Bb]atch \d+)\b/g;
72
+ const sections: string[] = [];
73
+ let cursor = 0;
74
+ for (;;) {
75
+ const match = pattern.exec(text);
76
+ if (!match) break;
77
+ if (match.index > cursor) sections.push(theme.fg("error", text.slice(cursor, match.index)));
78
+ sections.push(theme.fg("warning", match[0]));
79
+ cursor = match.index + match[0].length;
80
+ }
81
+ if (cursor < text.length) sections.push(theme.fg("error", text.slice(cursor)));
82
+ return sections.join("");
83
+ }
84
+
69
85
  export function fmtCall(
70
86
  args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | undefined,
71
87
  state: RRState,
@@ -144,6 +160,14 @@ function extractSummary(text: string | undefined): string | undefined {
144
160
  return summary.length > 0 ? summary : undefined;
145
161
  }
146
162
 
163
+ const BATCH_HEADER_RE = /^batch \d+:\n/;
164
+
165
+ function splitBatchHeader(diff: string, lineNumbers: (number | undefined)[] | undefined): { header: string | undefined; body: string; lineNumbers: (number | undefined)[] | undefined } {
166
+ const match = diff.match(BATCH_HEADER_RE);
167
+ if (!match) return { header: undefined, body: diff, lineNumbers };
168
+ return { header: match[0]!.slice(0, -1), body: diff.slice(match[0].length), lineNumbers: lineNumbers?.slice(1) };
169
+ }
170
+
147
171
  export function buildAppliedText(
148
172
  text: string | undefined,
149
173
  details: ReplaceDetails | undefined,
@@ -154,8 +178,9 @@ export function buildAppliedText(
154
178
  const summary = extractSummary(text);
155
179
  if (summary) sections.push(summary);
156
180
  if (details?.diff) {
157
- const rawDiff = details.diffLineNumbers ? toNumberedDiff(details.diff, details.diffLineNumbers) : details.diff;
158
- const diffLines = details.diff.split("\n");
181
+ const { header, body, lineNumbers } = splitBatchHeader(details.diff, details.diffLineNumbers);
182
+ const rawDiff = lineNumbers ? toNumberedDiff(body, lineNumbers) : body;
183
+ const diffLines = body.split("\n");
159
184
  const diffSection = expanded
160
185
  ? fmtResult(rawDiff, theme)
161
186
  : fmtPreview(rawDiff, false, theme);
@@ -163,7 +188,7 @@ export function buildAppliedText(
163
188
  !expanded && diffLines.length > RESULT_PREVIEW_LINES
164
189
  ? ` (${expandHint()})`
165
190
  : "";
166
- sections.push(`${diffSection}${hint}`);
191
+ sections.push(header !== undefined ? `${theme.fg("warning", header)}\n${diffSection}${hint}` : `${diffSection}${hint}`);
167
192
  }
168
193
  const warnings = details?.warnings?.length ? `Warnings:\n${details.warnings.join("\n")}` : extractWarnings(text);
169
194
  if (warnings) sections.push(warnings);
@@ -343,9 +368,17 @@ export function renderEditResult(
343
368
  renderState.preview = undefined;
344
369
  renderState.previewGeneration = (renderState.previewGeneration ?? 0) + 1;
345
370
  }
371
+ const batch = result.details?.batch;
372
+ if (batch && batch.last === false) {
373
+ const abortedMessage = typeof context?.toolCallId === "string" ? abortedBatchMessageFor(context.toolCallId) : undefined;
374
+ if (abortedMessage !== undefined) {
375
+ return reuseText(context, `\n${highlightBatchRefs(abortedMessage, theme)}`);
376
+ }
377
+ return reuseText(context, theme.fg("warning", `In batch ${batch.id}`));
378
+ }
346
379
  if (context.isError) {
347
380
  return renderedText
348
- ? reuseText(context, `\n${theme.fg("error", renderedText)}`)
381
+ ? reuseText(context, `\n${highlightBatchRefs(renderedText, theme)}`)
349
382
  : new Text("", 0, 0);
350
383
  }
351
384
  if (isApplied(result.details)) {
@@ -2,7 +2,7 @@ import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
2
2
  import type { NEdit } from "./hashline";
3
3
  import { HASH_SEP } from "./hashline";
4
4
  import type { ReplaceDetails } from "./replace";
5
- import { genDiff, genPatch, type DiffSpan } from "./replace-diff";
5
+ import { genDiff, genPatch, fmtDedupRow, type DiffSpan } from "./replace-diff";
6
6
  import { visLines, clipLine } from "./utils";
7
7
  import { DEDUP_ANCHOR } from "./constants";
8
8
 
@@ -106,7 +106,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
106
106
  : "The edit produced identical content.";
107
107
  const dedupNote =
108
108
  boundaryRemovedLines !== undefined && boundaryRemovedLines > 0
109
- ? `\nBoundary dedup removed ${boundaryRemovedLines} line(s); resend the same edit to apply it literally.`
109
+ ? `\nBoundary dedup removed ${boundaryRemovedLines} line(s) that duplicated adjacent lines.`
110
110
  : "";
111
111
 
112
112
  const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${dedupNote}${warnBlock(warnings)}`;
@@ -131,13 +131,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
131
131
  },
132
132
  };
133
133
  }
134
- export function fmtDedupRow(line: string): string {
135
- const row = `${DEDUP_ANCHOR}${HASH_SEP}${line}`;
136
- if (Buffer.byteLength(row, "utf-8") <= DEFAULT_MAX_BYTES) return row;
137
- const size = formatSize(Buffer.byteLength(row, "utf-8"));
138
- const limit = formatSize(DEFAULT_MAX_BYTES);
139
- return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
140
- }
134
+ export { fmtDedupRow };
141
135
 
142
136
  export function isDedupRow(line: string): boolean {
143
137
  return line.startsWith(`${DEDUP_ANCHOR}${HASH_SEP}`);
@@ -147,9 +141,42 @@ export function isChangeRow(line: string): boolean {
147
141
  return line.startsWith("+") || line.startsWith("-");
148
142
  }
149
143
 
150
- export function withDedupRows(diff: string, lineNumbers: (number | undefined)[], above: string[] | undefined, below: string[] | undefined): { diff: string; lineNumbers: (number | undefined)[] } {
151
- const top = (above ?? []).map(fmtDedupRow);
152
- const bottom = (below ?? []).map(fmtDedupRow);
144
+ function capDedupRows(rows: string[], budget: number): { rows: string[]; bytes: number; omitted: number } {
145
+ const kept: string[] = [];
146
+ let bytes = 0;
147
+ for (const row of rows) {
148
+ const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
149
+ if (bytes + rowBytes > budget) break;
150
+ kept.push(row);
151
+ bytes += rowBytes;
152
+ }
153
+ return { rows: kept, bytes, omitted: rows.length - kept.length };
154
+ }
155
+
156
+ function dedupOmissionNote(omitted: number, maxBytes: number): string {
157
+ return fmtDedupRow(`[${omitted} line(s) not shown again; output capped at ${formatSize(maxBytes)}.]`);
158
+ }
159
+
160
+ export function withDedupRows(diff: string, lineNumbers: (number | undefined)[], above: string[] | undefined, below: string[] | undefined, maxBytes = DEFAULT_MAX_BYTES): { diff: string; lineNumbers: (number | undefined)[] } {
161
+ const topAll = (above ?? []).map(fmtDedupRow);
162
+ const bottomAll = (below ?? []).map(fmtDedupRow);
163
+ if (topAll.length === 0 && bottomAll.length === 0) return { diff, lineNumbers };
164
+ const usedBytes = Buffer.byteLength(diff, "utf-8");
165
+ const fullBudget = Math.max(0, maxBytes - usedBytes);
166
+ const initialTop = capDedupRows(topAll, fullBudget);
167
+ const initialBottom = capDedupRows(bottomAll, Math.max(0, fullBudget - initialTop.bytes));
168
+ const anyOmitted = initialTop.omitted + initialBottom.omitted > 0;
169
+ const reserved = anyOmitted ? Buffer.byteLength(dedupOmissionNote(topAll.length + bottomAll.length, maxBytes), "utf-8") + 1 : 0;
170
+ const budget = Math.max(0, fullBudget - reserved);
171
+ const topCapped = anyOmitted ? capDedupRows(topAll, budget) : initialTop;
172
+ const bottomCapped = anyOmitted ? capDedupRows(bottomAll, Math.max(0, budget - topCapped.bytes)) : initialBottom;
173
+ const top = topCapped.rows;
174
+ const bottom = [...bottomCapped.rows];
175
+ const omitted = topCapped.omitted + bottomCapped.omitted;
176
+ if (omitted > 0) {
177
+ const note = dedupOmissionNote(omitted, maxBytes);
178
+ if (usedBytes + topCapped.bytes + bottomCapped.bytes + Buffer.byteLength(note, "utf-8") + 1 <= maxBytes) bottom.push(note);
179
+ }
153
180
  if (top.length === 0 && bottom.length === 0) return { diff, lineNumbers };
154
181
  if (diff.length === 0) return { diff: [...top, ...bottom].join("\n"), lineNumbers: [...lineNumbers, ...[...top, ...bottom].map(() => undefined)] };
155
182
  const lines = diff.split("\n");
@@ -175,7 +202,9 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
175
202
  const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes, boundaryDedupAbove, boundaryDedupBelow, spans } = input;
176
203
  const resultLines = visLines(result);
177
204
  const baseDiff = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes, undefined, spans);
178
- const diffResult = withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
205
+ const diffResult = baseDiff.spanDedupEmitted
206
+ ? baseDiff
207
+ : withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
179
208
  const addedLines = editMeta.addedLines;
180
209
  const removedLines = editMeta.removedLines;
181
210
  const warningsBlock = warnBlock(warnings);
@@ -4,10 +4,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
6
  import { loadHashStore, persistSnapshot, upsertUndo, getUndoEntry, deleteUndo, type UndoRecord } from "./hash-store";
7
- import { servedHashesFromDiff, buildServedMap } from "./served";
8
- import { contentChecksum } from "./hashline/hasher";
9
- import { hashSource } from "./hashline";
10
- import { markServed as markServedScoped, freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
7
+ import { servedHashesFromDiff, serveRows } from "./served";
8
+ import { lineChecksum } from "./hashline";
9
+ import { freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
11
10
  import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
12
11
  import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
13
12
  import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
@@ -172,7 +171,7 @@ export function regUndo(pi: ExtensionAPI): void {
172
171
  content: [
173
172
  {
174
173
  type: "text",
175
- text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted. The current content already contains your applied edit plus that external change and is most likely the correct state. Do not modify the file to make an undo possible and do not revert your own edit. The undo record is kept. Call read() to verify the current state, then stop.`
174
+ text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted and the file was left untouched. The undo record is kept. Do not edit the file to force the undo. Call read() to inspect the current state.`
176
175
  },
177
176
  ],
178
177
  isError: true,
@@ -200,16 +199,17 @@ export function regUndo(pi: ExtensionAPI): void {
200
199
  try {
201
200
  const store = await loadHashStore();
202
201
  const undoLines = splitLines(undo.content);
203
- persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map((line) => contentChecksum(hashSource(line))));
202
+ persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map(lineChecksum));
204
203
  freeAnchors(mutationTargetPath);
205
204
  adoptAnchors(
206
205
  mutationTargetPath,
207
- new Map(undoLines.map((line, i) => [undo.hashes[i]!, contentChecksum(hashSource(line))])),
206
+ new Map(undoLines.map((line, i) => [undo.hashes[i]!, lineChecksum(line)])),
208
207
  );
209
- markServedScoped(
208
+ serveRows(
210
209
  mutationTargetPath,
211
- buildServedMap(undo.hashes, undoLines, servedHashesFromDiff(undoDiff)),
212
- new Set(undo.hashes),
210
+ undo.hashes,
211
+ undoLines,
212
+ servedHashesFromDiff(undoDiff),
213
213
  );
214
214
  } catch (error) {
215
215
  console.error("Failed to restore hash store snapshot after undo:", error);
package/src/replace.ts CHANGED
@@ -32,10 +32,9 @@ import { loadHashStore, type HashStore } from "./hash-store";
32
32
  import { adoptAnchors, servedForPath, withAnchorSession } from "./anchor-registry";
33
33
  import { resolveTarget } from "./fs-write";
34
34
  import { toCwd } from "./paths";
35
- import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
36
35
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
37
- import { commitEdit } from "./commit";
38
- import { batchMemberFor, executeBatchMember, noteBatchFailure, suffixPoisonCause } from "./batch";
36
+ import { commitEdit, dedupRowsFromFixes } from "./commit";
37
+ import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
39
38
 
40
39
  export { editToolSchema, type ReqParams, assertReq };
41
40
 
@@ -67,7 +66,6 @@ export interface PipelineResult {
67
66
  resultHashes: string[];
68
67
  totalAddedLines: number;
69
68
  totalRemovedLines: number;
70
- hadBoundaryDedup: boolean;
71
69
  boundaryRemovedLines: number;
72
70
  boundaryRemovedLineTexts: string[];
73
71
  boundaryDedupAbove: string[];
@@ -92,7 +90,14 @@ export function hashSpan(hashes: string[], from: string, to: string): [number, n
92
90
  if (a < 0 || b < 0) return undefined;
93
91
  return [Math.min(a, b), Math.max(a, b)];
94
92
  }
95
- async function noteAnchorError(absolutePath: string, error: unknown, scopeHashes: string[], noPersist?: boolean): Promise<void> {
93
+
94
+ export function spanForEdit(originalHashes: string[], from: string, to: string, resultContent: string): DiffSpan | undefined {
95
+ const span = hashSpan(originalHashes, from, to);
96
+ if (!span) return undefined;
97
+ const replacementCount = splitLines(resultContent).length - (originalHashes.length - (span[1] - span[0] + 1));
98
+ return { start: span[0], end: span[1], replacementCount };
99
+ }
100
+ async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
96
101
  if (noPersist === true) return;
97
102
  if (error instanceof RangeStaleError) {
98
103
  adoptAnchors(absolutePath, error.rangeServedMap);
@@ -161,7 +166,7 @@ export async function execPipeline(
161
166
  strictBoundaryDedup,
162
167
  );
163
168
  } catch (error) {
164
- await noteAnchorError(absolutePath, error, originalHashes, options?.noPersist);
169
+ await noteAnchorError(absolutePath, error, options?.noPersist);
165
170
  throw error;
166
171
  }
167
172
 
@@ -180,12 +185,9 @@ export async function execPipeline(
180
185
  edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
181
186
  );
182
187
 
183
- const sortedFixes = [...(anchorResult.autoFixes ?? [])].sort((a, b) => a.removedLineIndex - b.removedLineIndex);
184
- const aboveFixes = sortedFixes.filter((fix) => fix.kind === "leading" || fix.kind === "last-new-before");
185
- const belowFixes = sortedFixes.filter((fix) => fix.kind === "trailing" || fix.kind === "first-new-after");
186
- const pipeSpan = isNoop ? undefined : hashSpan(originalHashes, edit.hash_bounds[0].hash, edit.hash_bounds[1].hash);
187
- const pipeResultCount = splitLines(result).length;
188
- const pipeSpans = pipeSpan ? [{ start: pipeSpan[0], end: pipeSpan[1], replacementCount: pipeResultCount - (originalHashes.length - (pipeSpan[1] - pipeSpan[0] + 1)) }] : undefined;
188
+ const sortedFixes = dedupRowsFromFixes(anchorResult.autoFixes ?? []);
189
+ const pipeSpan = isNoop ? undefined : spanForEdit(originalHashes, edit.hash_bounds[0].hash, edit.hash_bounds[1].hash, result);
190
+ const pipeSpans = pipeSpan ? [pipeSpan] : undefined;
189
191
  return {
190
192
  path: displayPath,
191
193
  originalNormalized,
@@ -201,11 +203,10 @@ export async function execPipeline(
201
203
  originalHashes,
202
204
  totalAddedLines,
203
205
  totalRemovedLines,
204
- hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
205
206
  boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
206
- boundaryRemovedLineTexts: sortedFixes.map((fix) => fix.removedLine),
207
- boundaryDedupAbove: aboveFixes.map((fix) => fix.removedLine),
208
- boundaryDedupBelow: belowFixes.map((fix) => fix.removedLine),
207
+ boundaryRemovedLineTexts: sortedFixes.removedTexts,
208
+ boundaryDedupAbove: sortedFixes.above,
209
+ boundaryDedupBelow: sortedFixes.below,
209
210
  identity,
210
211
  ...(pipeSpans ? { spans: pipeSpans } : {}),
211
212
  };
@@ -287,54 +288,34 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
287
288
  }).catch((error: unknown) => {
288
289
  const member = batchMemberFor(_toolCallId);
289
290
  if (member) noteBatchFailure(member, error);
290
- else suffixPoisonCause(_toolCallId, error);
291
291
  throw error;
292
292
  });
293
293
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
294
294
  const dedupMode = await getBoundaryDedupMode();
295
- const dedupOn = dedupMode !== "off";
296
- const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
297
- const boundaryBypass = dedupOn ? consumeBoundaryBypass(mutationTargetPath, noopPayload) : false;
298
- const strictBoundaryDedup = dedupMode === "strict" && !boundaryBypass;
295
+ const strictBoundaryDedup = dedupMode === "strict";
299
296
  const member = batchMemberFor(_toolCallId);
300
297
  if (!member) {
301
- try {
302
- const pipe = await execPipeline(
303
- targetPath,
304
- normalizedParams,
305
- ctx.cwd,
306
- { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
307
- );
308
- const appliedWarnings = boundaryBypass
309
- ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
310
- : [];
311
- return await commitEdit(pipe, {
312
- path: pipe.path,
313
- absolutePath,
314
- mutationTargetPath,
315
- editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
316
- signal,
317
- appliedWarnings,
318
- onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
319
- onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
320
- });
321
- } catch (error) {
322
- const detail = error instanceof Error ? error.message : String(error);
323
- if (boundaryBypass && !detail.includes("File was written;")) markBoundaryNoop(mutationTargetPath, noopPayload);
324
- throw error;
325
- }
298
+ const pipe = await execPipeline(
299
+ targetPath,
300
+ normalizedParams,
301
+ ctx.cwd,
302
+ { accessMode: constants.R_OK | constants.W_OK, signal },
303
+ );
304
+ return await commitEdit(pipe, {
305
+ path: pipe.path,
306
+ absolutePath,
307
+ mutationTargetPath,
308
+ editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
309
+ signal,
310
+ });
326
311
  }
327
312
  let built: { edit: HEdit; warnings: string[] };
328
313
  try {
329
314
  built = buildReplaceHEdit(normalizedParams);
330
315
  } catch (error) {
331
316
  noteBatchFailure(member, error);
332
- if (boundaryBypass) markBoundaryNoop(mutationTargetPath, noopPayload);
333
317
  throw error;
334
318
  }
335
- const appliedWarnings = boundaryBypass
336
- ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
337
- : [];
338
319
  return executeBatchMember({
339
320
  kind: "replace",
340
321
  member,
@@ -343,11 +324,9 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
343
324
  cwd: ctx.cwd,
344
325
  signal,
345
326
  hedit: built.edit,
346
- extraWarnings: [...built.warnings, ...appliedWarnings],
347
- skipBoundaryDedup: boundaryBypass,
327
+ extraWarnings: built.warnings,
328
+ skipBoundaryDedup: false,
348
329
  strictBoundaryDedup,
349
- noopPayload,
350
- bypassConsumed: boundaryBypass,
351
330
  });
352
331
  });
353
332
  });
package/src/served.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { HASH_CLASS } from "./hashline/alphabet";
2
- import { hashSource } from "./hashline";
3
- import { contentChecksum } from "./hashline/hasher";
2
+ import { lineChecksum } from "./hashline/hash";
3
+ import { markServed } from "./anchor-registry";
4
4
 
5
5
  const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
6
6
 
@@ -26,7 +26,11 @@ export function buildServedMap(
26
26
  const entries: Array<[string, string]> = [];
27
27
  for (const hash of wantedHashes) {
28
28
  const idx = index.get(hash);
29
- if (idx !== undefined) entries.push([hash, contentChecksum(hashSource(fileLines[idx]!))]);
29
+ if (idx !== undefined) entries.push([hash, lineChecksum(fileLines[idx]!)]);
30
30
  }
31
31
  return entries;
32
32
  }
33
+
34
+ export function serveRows(path: string, fileHashes: string[], fileLines: string[], wantedHashes: string[]): void {
35
+ markServed(path, buildServedMap(fileHashes, fileLines, wantedHashes), new Set(fileHashes));
36
+ }
package/src/utils.ts CHANGED
@@ -22,14 +22,16 @@ export function normalizeAnchors(record: Record<string, unknown>): void {
22
22
  }
23
23
  }
24
24
 
25
+ export function normalizeRequest(input: unknown): unknown {
26
+ if (!isRec(input)) return input;
27
+ const record: Record<string, unknown> = { ...input };
28
+ normalizeFilePath(record);
29
+ normalizeAnchors(record);
30
+ return record;
31
+ }
32
+
25
33
  export function makePrepareArguments(): (args: unknown) => any {
26
- return (args) => {
27
- if (!isRec(args)) return args;
28
- const record = { ...args };
29
- normalizeFilePath(record);
30
- normalizeAnchors(record);
31
- return record;
32
- };
34
+ return normalizeRequest;
33
35
  }
34
36
 
35
37
  export function splitLines(text: string): string[] {
@@ -293,7 +295,6 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
293
295
  if (candidate === undefined) return undefined;
294
296
  const decoded = decodeArrayText(candidate);
295
297
  if (decoded !== undefined) {
296
- warnings?.push(`[W_BAD_SHAPE] Unwrapped JSON array syntax from a ${label} element.`);
297
298
  return decoded;
298
299
  }
299
300
  if (looksLikeStringArray(candidate)) {
package/src/write-hook.ts CHANGED
@@ -19,10 +19,6 @@ export function findServedHashEcho(content: string, served: ReadonlyMap<string,
19
19
  return searchEcho(splitLines(content), served);
20
20
  }
21
21
 
22
- export function findEditHashEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
23
- return searchEcho(lines, served);
24
- }
25
-
26
22
  export async function servedHashEchoDenial(rawPath: string, content: string, cwd: string, signal?: AbortSignal): Promise<string | undefined> {
27
23
  abortIf(signal);
28
24
  const { resolved } = await resolveInCwd(rawPath, cwd);
@@ -1,51 +0,0 @@
1
- import { parseText } from "./hashline/parse";
2
- import { ANCHOR_ROW_RE } from "./hashline/resolve";
3
- import { stripRowPrefix } from "./hashline/hash";
4
-
5
- function canonRef(ref: string): string {
6
- const trimmed = ref.trim();
7
- const match = trimmed.match(ANCHOR_ROW_RE);
8
- return match ? match[2]! : trimmed;
9
- }
10
-
11
- function canonLines(lines: string[]): string[] {
12
- return parseText(lines).map((line) => stripRowPrefix(line).text);
13
- }
14
-
15
- const boundaryBypassTracker = new Map<string, string>();
16
- const BOUNDARY_BYPASS_LIMIT = 256;
17
-
18
- export function noopPayloadKey(
19
- absolutePath: string,
20
- removeFrom: string,
21
- removeTo: string,
22
- replacementLines: string[],
23
- ): string {
24
- return JSON.stringify([
25
- absolutePath,
26
- canonRef(removeFrom),
27
- canonRef(removeTo),
28
- canonLines(replacementLines),
29
- ]);
30
- }
31
-
32
- export function markBoundaryNoop(absolutePath: string, payload: string): void {
33
- boundaryBypassTracker.delete(absolutePath);
34
- boundaryBypassTracker.set(absolutePath, payload);
35
- if (boundaryBypassTracker.size > BOUNDARY_BYPASS_LIMIT) {
36
- const oldest = boundaryBypassTracker.keys().next().value;
37
- if (oldest !== undefined) boundaryBypassTracker.delete(oldest);
38
- }
39
- }
40
-
41
- export function consumeBoundaryBypass(absolutePath: string, payload: string): boolean {
42
- if (boundaryBypassTracker.get(absolutePath) === payload) {
43
- boundaryBypassTracker.delete(absolutePath);
44
- return true;
45
- }
46
- return false;
47
- }
48
-
49
- export function clearBoundaryBypass(absolutePath: string): void {
50
- boundaryBypassTracker.delete(absolutePath);
51
- }