pi-hashline-edit-pro 3.0.3 → 4.0.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.
@@ -167,7 +167,10 @@ export function applyEdit(
167
167
  abortIf(signal);
168
168
 
169
169
  const lineIndex = buildIdx(content);
170
- const fileHashes = precomputedHashes ?? _lineHashesPure(content);
170
+ if (precomputedHashes === undefined) {
171
+ throw new Error("[E_BAD_SHAPE] applyEdit requires the file's allocated anchors; derive them via lineHashes(content, path) first.");
172
+ }
173
+ const fileHashes = precomputedHashes;
171
174
  const warnings: string[] = [];
172
175
 
173
176
  const rangeFixed = swapReversedRanges(edit, fileHashes, warnings);
@@ -1,13 +1,9 @@
1
1
  import { splitLines, truncateToBytes, getCached } from "../utils";
2
2
  import { MAX_HASH_SOURCE_BYTES } from "../constants";
3
- import {
4
- loadHashStore,
5
- type HashStore,
6
- getSnapshot,
7
- persistSnapshot,
8
- } from "../hash-store";
3
+ import { loadHashStore, type HashStore } from "../hash-store";
4
+ import { allocateFileAnchors } from "../anchor-registry";
9
5
  import { xxh32, initHasher } from "./hasher";
10
- import { HASH_LEN, ANCHOR_COUNT, anchorAt, anchorIndex, HASH_CLASS, HASH_RUN } from "./alphabet";
6
+ import { HASH_LEN, ANCHOR_COUNT, anchorAt, HASH_CLASS, HASH_RUN } from "./alphabet";
11
7
  export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
12
8
 
13
9
  export const ANCHOR_LEN = HASH_LEN;
@@ -17,7 +13,8 @@ export const HASH_SEP = "│";
17
13
  export const HASH_SPACE = ANCHOR_COUNT;
18
14
  export const MAX_HASH_LINES = HASH_SPACE;
19
15
 
20
- export const HASH_PROBE_STRIDE = 571;
16
+ export const HASH_PROBE_STRIDE = 836286;
17
+
21
18
 
22
19
  function hashAt(idx: number): string {
23
20
  return anchorAt(idx);
@@ -117,179 +114,19 @@ export function _lineHashesPure(content: string): string[] {
117
114
  export async function lineHashes(
118
115
  content: string,
119
116
  path?: string,
120
- previous?: { content: string; hashes: string[]; removedHashes?: Set<string> },
117
+ previous?: { content: string; hashes: string[]; spans?: { start: number; end: number; replacementCount: number }[] },
121
118
  store?: HashStore,
122
119
  persist?: boolean,
120
+ shadow?: boolean,
123
121
  ): Promise<string[]> {
124
122
  await initHasher();
125
123
  if (!path) {
126
124
  return _lineHashesPure(content);
127
125
  }
128
-
129
- const hashStore = store ?? await loadHashStore();
130
-
131
- if (previous) {
132
- const newHashes = mapStableHashes(
133
- previous.content, previous.hashes,
134
- content,
135
- previous.removedHashes,
136
- );
137
- if (persist !== false) {
138
- try {
139
- persistSnapshot(hashStore, path, content, newHashes);
140
- } catch (error) {
141
- console.error("Failed to persist hash snapshot:", error);
142
- }
143
- }
144
- return newHashes;
145
- }
146
-
147
- let cached: string[] | undefined;
148
- try {
149
- cached = getSnapshot(hashStore, path, content, persist !== false);
150
- } catch (error) {
151
- console.error("Failed to read hash store snapshot:", error);
152
- }
153
- if (cached) {
154
- return cached;
155
- }
156
-
157
- const newHashes = _lineHashesPure(content);
158
- if (persist !== false) {
159
- try {
160
- persistSnapshot(hashStore, path, content, newHashes);
161
- } catch (error) {
162
- console.error("Failed to persist hash snapshot:", error);
163
- }
164
- }
165
- return newHashes;
166
- }
167
-
168
- function hashToIndex(hash: string): number {
169
- return anchorIndex(hash);
170
- }
171
-
172
- function nearestNew(
173
- candidates: number[],
174
- target: number,
175
- ): number {
176
- let lo = 0;
177
- let hi = candidates.length;
178
- while (lo < hi) {
179
- const mid = (lo + hi) >>> 1;
180
- if (candidates[mid]! < target) lo = mid + 1;
181
- else hi = mid;
182
- }
183
- const left = lo - 1;
184
- const right = lo;
185
- if (
186
- left >= 0 &&
187
- (right >= candidates.length ||
188
- target - candidates[left]! <= candidates[right]! - target)
189
- ) {
190
- return left;
191
- }
192
- return right < candidates.length ? right : -1;
126
+ return allocateFileAnchors(store ?? (await loadHashStore()), path, content, {
127
+ persist,
128
+ shadow,
129
+ previous,
130
+ });
193
131
  }
194
132
 
195
- function mapStableHashes(
196
- oldContent: string,
197
- oldHashes: string[],
198
- newContent: string,
199
- removedHashes?: Set<string>,
200
- ): string[] {
201
- const oldLines = splitLines(oldContent);
202
- const newLines = splitLines(newContent);
203
- const newHashes = new Array<string>(newLines.length);
204
- const used = new Uint32Array(BITSET_WORDS);
205
- const hint = { value: 0 };
206
- const hashSourceCache = new Map<string, string>();
207
- const removed = removedHashes ?? new Set<string>();
208
-
209
- const oldHashIndex = new Map<string, number>();
210
- for (let i = 0; i < oldHashes.length; i++) {
211
- const hash = oldHashes[i]!;
212
- oldHashIndex.set(hash, i);
213
- const idx = hashToIndex(hash);
214
- if (idx >= 0) setBit(used, idx);
215
- }
216
-
217
- const removedIndexes = new Set<number>();
218
- for (const hash of removed) {
219
- const idx = oldHashIndex.get(hash);
220
- if (idx !== undefined) removedIndexes.add(idx);
221
- }
222
-
223
- let spanStart = oldLines.length;
224
- let spanEnd = -1;
225
- for (const idx of removedIndexes) {
226
- if (idx < spanStart) spanStart = idx;
227
- if (idx > spanEnd) spanEnd = idx;
228
- }
229
- const spanLen = spanEnd >= spanStart ? spanEnd - spanStart + 1 : 0;
230
- const replacementLen = newLines.length - oldLines.length + spanLen;
231
- const shiftAfterSpan = spanEnd >= spanStart ? replacementLen - spanLen : 0;
232
-
233
- const survivors: { index: number; hash: string }[] = [];
234
- const removedEntries: { index: number; hash: string }[] = [];
235
- for (let i = 0; i < oldLines.length; i++) {
236
- const entry = { index: i, hash: oldHashes[i]! };
237
- if (removedIndexes.has(i)) removedEntries.push(entry);
238
- else survivors.push(entry);
239
- }
240
-
241
- const newByContent = new Map<string, number[]>();
242
- for (let i = 0; i < newLines.length; i++) {
243
- const key = getCached(hashSourceCache, newLines[i]!, hashSource);
244
- const list = newByContent.get(key);
245
- if (list) list.push(i);
246
- else newByContent.set(key, [i]);
247
- }
248
-
249
- const markUsed = (hash: string): void => {
250
- const idx = hashToIndex(hash);
251
- if (idx >= 0) {
252
- setBit(used, idx);
253
- if (idx + HASH_PROBE_STRIDE > hint.value) hint.value = idx + HASH_PROBE_STRIDE;
254
- }
255
- };
256
-
257
- for (const entry of survivors) {
258
- const candidates = newByContent.get(getCached(hashSourceCache, oldLines[entry.index]!, hashSource));
259
- if (!candidates || candidates.length === 0) continue;
260
- const target = entry.index > spanEnd ? entry.index + shiftAfterSpan : entry.index;
261
- const pos = nearestNew(candidates, target);
262
- if (pos < 0) continue;
263
- const newIdx = candidates.splice(pos, 1)[0]!;
264
- newHashes[newIdx] = entry.hash;
265
- markUsed(entry.hash);
266
- }
267
-
268
- const removedByContent = new Map<string, { hashes: string[]; pos: number }>();
269
- for (const entry of removedEntries) {
270
- const key = oldLines[entry.index]!;
271
- let queue = removedByContent.get(key);
272
- if (!queue) {
273
- queue = { hashes: [], pos: 0 };
274
- removedByContent.set(key, queue);
275
- }
276
- queue.hashes.push(entry.hash);
277
- }
278
-
279
- for (let i = 0; i < newLines.length; i++) {
280
- if (newHashes[i]) continue;
281
- const queue = removedByContent.get(newLines[i]!);
282
- if (!queue || queue.pos >= queue.hashes.length) continue;
283
- newHashes[i] = queue.hashes[queue.pos]!;
284
- queue.pos += 1;
285
- }
286
-
287
- for (let i = 0; i < newLines.length; i++) {
288
- if (newHashes[i]) continue;
289
- const c = getCached(hashSourceCache, newLines[i]!, hashSource);
290
- const baseIdx = (xxh32(c) >>> 14) % HASH_SPACE;
291
- newHashes[i] = assignHash(used, baseIdx, hint);
292
- }
293
-
294
- return newHashes;
295
- }
@@ -63,14 +63,14 @@ function unwrapJsonEnvelope(line: string, warnings?: string[]): string {
63
63
  const parsed: unknown = JSON.parse(withoutDot);
64
64
  if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string") {
65
65
  warnings?.push(
66
- '[E_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
66
+ '[W_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
67
67
  );
68
68
  return parsed[0];
69
69
  }
70
70
  return line;
71
71
  } catch {
72
72
  warnings?.push(
73
- '[E_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
73
+ '[W_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
74
74
  );
75
75
  return match[1]!;
76
76
  }
@@ -85,12 +85,12 @@ export function parseText(edit: string[], warnings?: string[]): string[] {
85
85
  for (const line of edit) {
86
86
  const unwrapped = unwrapJsonEnvelope(line, warnings);
87
87
  const normalized = unwrapped.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
88
- if (normalized !== unwrapped) split = true;
88
+ if (normalized.includes("\n")) split = true;
89
89
  out.push(...normalized.split("\n"));
90
90
  }
91
91
  if (split) {
92
92
  warnings?.push(
93
- "[E_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
93
+ "[W_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
94
94
  );
95
95
  }
96
96
  return out;
@@ -3,6 +3,7 @@ import { HASH_SEP, HASH_RUN, stripRowPrefix, canon } from "./hash";
3
3
  import { parseHashRef, parseText, type Anchor } from "./parse";
4
4
  import { NEW_CONTENT_NOT_ARRAY_MSG, MAX_RANGE_STALE_LINES } from "../constants";
5
5
  import { contentChecksum } from "./hasher";
6
+ import { hashSource } from "./hash";
6
7
 
7
8
  export type RAnchor = {
8
9
  line: number;
@@ -17,8 +18,7 @@ export type RHEdit = {
17
18
 
18
19
  interface HMismatch {
19
20
  ref: Anchor;
20
- kind: "not_found" | "ambiguous";
21
- candidates?: number[];
21
+ kind: "not_found";
22
22
  context?: RAnchor;
23
23
  }
24
24
 
@@ -52,13 +52,10 @@ function resAnchorFromMap(
52
52
  if (!hashMatches || hashMatches.length === 0) {
53
53
  return { ref, kind: "not_found" };
54
54
  }
55
- if (hashMatches.length === 1) {
56
- return {
57
- line: hashMatches[0]!,
58
- hash: ref.hash,
59
- };
60
- }
61
- return { ref, kind: "ambiguous", candidates: hashMatches };
55
+ return {
56
+ line: hashMatches[0]!,
57
+ hash: ref.hash,
58
+ };
62
59
  }
63
60
 
64
61
  function assertAligned(
@@ -96,10 +93,9 @@ export function fmtMismatchWithHashes(
96
93
  const out: string[] = [];
97
94
  const hashes: string[] = [];
98
95
  const servedMap = new Map<string, string>();
99
- const notFound = mismatches.filter((m) => m.kind === "not_found");
100
- const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
101
- const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
96
+ const notFound = mismatches;
102
97
  if (notFound.length > 0) {
98
+ const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
103
99
  out.push(
104
100
  `[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. The file changed since read. Call read() for fresh anchors.`
105
101
  );
@@ -113,41 +109,13 @@ export function fmtMismatchWithHashes(
113
109
  const h = fileHashes[ln - 1]!;
114
110
  const c = fileLines[ln - 1] ?? "";
115
111
  hashes.push(h);
116
- servedMap.set(h, contentChecksum(c));
112
+ servedMap.set(h, contentChecksum(hashSource(c)));
117
113
  rows.push(` ${ln}: ${h}│${clipLine(c)}`);
118
114
  }
119
115
  out.push("");
120
116
  out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
121
117
  }
122
118
  }
123
- if (ambiguous.length > 0) {
124
- if (out.length > 0) out.push("");
125
- out.push(
126
- `[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Call read() for fresh anchors.`
127
- );
128
- for (const m of ambiguous) {
129
- const sample = (m.candidates ?? []).slice(0, 5);
130
- const more =
131
- (m.candidates?.length ?? 0) > sample.length
132
- ? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)`
133
- : "";
134
- for (const line of sample) {
135
- const h = fileHashes[line - 1]!;
136
- const c = fileLines[line - 1] ?? "";
137
- hashes.push(h);
138
- servedMap.set(h, contentChecksum(c));
139
- }
140
- const lines = sample
141
- .map((line) => {
142
- const content = clipLine(fileLines[line - 1] ?? "");
143
- return ` ${line}: ${fileHashes[line - 1]}│${content}`;
144
- })
145
- .join("\n");
146
- out.push(
147
- ` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`,
148
- );
149
- }
150
- }
151
119
  return { text: out.join("\n"), hashes, servedMap };
152
120
  }
153
121
 
@@ -195,7 +163,7 @@ export function stripAnchorRow(
195
163
  : match[1] === "-"
196
164
  ? 'leading "-" marker'
197
165
  : '"anchor│" prefix';
198
- warnings?.push(`[E_BAD_REF] Stripped ${marker} from ${entryLabel} "${clipLine(trimmed, 48)}".`);
166
+ warnings?.push(`[W_BAD_REF] Stripped ${marker} from ${entryLabel} "${clipLine(trimmed, 48)}".`);
199
167
  return match[2]!;
200
168
  }
201
169
 
@@ -241,7 +209,7 @@ export function stripBarePrefixes(
241
209
  .map((s) => `replacement_lines line ${s.lineIndex + 1}`)
242
210
  .join(", ");
243
211
  warnings.push(
244
- `[E_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.`
212
+ `[W_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.`
245
213
  );
246
214
  return { ...edit, content_lines: contentLines };
247
215
  }
@@ -260,7 +228,7 @@ export function stripDiffPrefixes(
260
228
  if (stripped.length === 0) return edit;
261
229
  const locations = stripped.map((i) => `replacement_lines line ${i + 1}`).join(", ");
262
230
  warnings.push(
263
- `[E_INVALID_PATCH] Stripped diff-preview marker from ${locations}.`
231
+ `[W_INVALID_PATCH] Stripped diff-preview marker from ${locations}.`
264
232
  );
265
233
  return { ...edit, content_lines: contentLines };
266
234
  }
@@ -285,7 +253,7 @@ export function swapReversedRanges(
285
253
  return edit;
286
254
  }
287
255
  warnings.push(
288
- `[E_BAD_OP] Swapped reversed remove_from/remove_to.`
256
+ `[W_BAD_OP] Swapped reversed remove_from/remove_to.`
289
257
  );
290
258
  return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
291
259
  }
@@ -545,7 +513,7 @@ export function assertRangeServed(
545
513
  const hash = fileHashes[line - 1]!;
546
514
  const content = fileLines[line - 1]!;
547
515
  const servedContent = served?.get(hash);
548
- if (servedContent === undefined || servedContent !== contentChecksum(content)) mismatchLines.push(line);
516
+ if (servedContent === undefined || servedContent !== contentChecksum(hashSource(content))) mismatchLines.push(line);
549
517
  }
550
518
  if (mismatchLines.length === 0) return;
551
519
  const rangeLength = endLine - startLine + 1;
@@ -557,7 +525,7 @@ export function assertRangeServed(
557
525
  const hash = fileHashes[line - 1]!;
558
526
  const content = fileLines[line - 1]!;
559
527
  shownHashes.push(hash);
560
- shownMap.set(hash, contentChecksum(content));
528
+ shownMap.set(hash, contentChecksum(hashSource(content)));
561
529
  rows.push(fmtRow(hash, clipLine(content)));
562
530
  }
563
531
  const location = filePath ? ` in ${filePath}` : "";
package/src/insert.ts CHANGED
@@ -10,14 +10,13 @@ import { loadP, loadGuide } from "./prompts";
10
10
  import { normReq } from "./payload-contract";
11
11
  import { decodeStringArray, isRec, rejectUnknownFields, splitLines } from "./utils";
12
12
  import { clearBoundaryBypass } from "./boundary-bypass";
13
- import { resolveInsertPath } from "./missing-path";
13
+ import { resolveEditTarget } from "./edit-common";
14
14
  import type { RPreview, RRState } from "./replace-render";
15
15
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper } from "./edit-common";
16
16
 
17
- const INSERT_KS = new Set(["path", "anchor", "direction", "lines"]);
17
+ const INSERT_KS = new Set(["anchor", "direction", "lines"]);
18
18
 
19
19
  export interface InsertReq {
20
- path: string;
21
20
  anchor: string;
22
21
  direction: "before" | "after";
23
22
  lines: string[];
@@ -28,9 +27,6 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
28
27
  throw new Error("[E_BAD_SHAPE] Insert request must be an object.");
29
28
  }
30
29
  rejectUnknownFields(request, INSERT_KS, "Insert request");
31
- if (typeof request.path !== "string" || request.path.length === 0) {
32
- throw new Error('[E_BAD_SHAPE] Insert request requires a non-empty "path" string.');
33
- }
34
30
  if (typeof request.anchor !== "string" || request.anchor.length === 0) {
35
31
  throw new Error('[E_BAD_SHAPE] Insert request requires an "anchor" string (4-char anchor from read output).');
36
32
  }
@@ -44,15 +40,9 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
44
40
 
45
41
  const insertToolSchema = Type.Object(
46
42
  {
47
- path: Type.Optional(
48
- Type.String({
49
- description:
50
- "Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback.",
51
- }),
52
- ),
53
43
  anchor: Type.String({
54
44
  description:
55
- 'Bare 4-char anchor from a read row like `Hasu│content`, never the content. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
45
+ 'Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. A pasted diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
56
46
  }),
57
47
  direction: Type.Union(
58
48
  [
@@ -86,12 +76,12 @@ function buildInsertEdit(
86
76
  req: InsertReq,
87
77
  preload: NormFile,
88
78
  ref: Anchor,
79
+ path: string,
89
80
  ): { editParams: ReqParams; anchorLine: string | undefined } {
90
81
  const fileLines = splitLines(preload.normalized);
91
- const line = resolveAnchorLine(ref, fileLines, preload.fileHashes, req.path);
82
+ const line = resolveAnchorLine(ref, fileLines, preload.fileHashes, path);
92
83
  const anchorLine = preload.normalized.length === 0 ? undefined : fileLines[line - 1];
93
84
  const editParams: ReqParams = {
94
- path: req.path,
95
85
  remove_from: ref.hash,
96
86
  remove_to: ref.hash,
97
87
  replacement_lines:
@@ -111,20 +101,17 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
111
101
  const expanded = decodeStringArray(normalized.lines);
112
102
  if (expanded) normalized.lines = expanded;
113
103
  }
114
- if (isRec(normalized)) {
115
- const resolution = await resolveInsertPath(normalized);
116
- if (resolution) normalized.path = resolution.path;
117
- }
118
104
  assertInsertReq(normalized);
119
105
  const { ref } = parseInsertAnchor(normalized.anchor);
120
- const preload = await readNormFile(normalized.path, cwd, {
106
+ const targetPath = resolveEditTarget(normalized.anchor);
107
+ const preload = await readNormFile(targetPath, cwd, {
121
108
  accessMode: constants.R_OK,
122
109
  maxLines: MAX_HASH_LINES,
123
110
  noPersist: true,
124
111
  signal,
125
112
  });
126
- const { editParams } = buildInsertEdit(normalized, preload, ref);
127
- const pipe = await execPipeline(editParams, cwd, {
113
+ const { editParams } = buildInsertEdit(normalized, preload, ref, targetPath);
114
+ const pipe = await execPipeline(targetPath, editParams, cwd, {
128
115
  accessMode: constants.R_OK,
129
116
  noPersist: true,
130
117
  preloadedNorm: preload,
@@ -181,38 +168,36 @@ export function buildInsertToolDef(): InsertToolDef {
181
168
  if (isRec(canonical)) {
182
169
  const expanded = decodeStringArray(canonical.lines);
183
170
  if (expanded) {
184
- insertWarnings.push('[E_BAD_SHAPE] Unwrapped JSON array syntax from a lines element.');
171
+ insertWarnings.push('[W_BAD_SHAPE] Unwrapped JSON array syntax from a lines element.');
185
172
  canonical.lines = expanded;
186
173
  }
187
174
  }
188
- const resolution = isRec(canonical) ? await resolveInsertPath(canonical) : undefined;
189
- if (resolution && isRec(canonical)) canonical.path = resolution.path;
190
175
  assertInsertReq(canonical);
191
176
  const req = canonical;
192
- const path = req.path;
177
+ const targetPath = resolveEditTarget(req.anchor);
193
178
  const { ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor);
194
- return queuedEdit(path, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
195
- const preload = await readNormFile(path, ctx.cwd, {
179
+ return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
180
+ const preload = await readNormFile(targetPath, ctx.cwd, {
196
181
  signal,
197
182
  accessMode: constants.R_OK | constants.W_OK,
198
183
  maxLines: MAX_HASH_LINES,
199
184
  });
200
- const { editParams, anchorLine } = buildInsertEdit(req, preload, ref);
201
- const pipe = await execPipeline(editParams, ctx.cwd, {
185
+ const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
186
+ const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
202
187
  accessMode: constants.R_OK | constants.W_OK,
203
188
  signal,
204
189
  preloadedNorm: preload,
205
190
  skipBoundaryDedup: true,
206
191
  });
207
192
  return commitEdit(pipe, {
208
- path,
193
+ path: pipe.path,
209
194
  absolutePath,
210
195
  mutationTargetPath,
211
196
  signal,
212
197
  verb: "inserted",
213
198
  noopNoun: "Insertion",
214
199
  foldedAnchorLines: anchorLine === undefined ? 0 : 1,
215
- prefixWarnings: [...(resolution ? [resolution.warning] : []), ...anchorWarnings, ...insertWarnings],
200
+ prefixWarnings: [...anchorWarnings, ...insertWarnings],
216
201
  onApplied: () => clearBoundaryBypass(mutationTargetPath),
217
202
  });
218
203
  });
package/src/paths.ts CHANGED
@@ -31,6 +31,10 @@ export function legacyHashStorePath(): string {
31
31
  return join(configDir(), "hash-store.json");
32
32
  }
33
33
 
34
+ export function sessionClaimsDir(): string {
35
+ return join(configDir(), "sessions");
36
+ }
37
+
34
38
  export function hashStoreDir(): string {
35
39
  return dirname(hashStorePath());
36
40
  }
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { isRec, normalizeFilePath, rejectUnknownFields } from "./utils";
2
+ import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields } from "./utils";
3
3
 
4
4
  const replacementLinesSchema = Type.Array(
5
5
  Type.String({
@@ -14,22 +14,16 @@ const replacementLinesSchema = Type.Array(
14
14
 
15
15
  const removeFromSchema = Type.String({
16
16
  description:
17
- "Bare 4-char anchor from a read row like `Hasu│content` (the leftmost column), never the row content. Marks the FIRST line to remove (inclusive)",
17
+ "Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. Marks the FIRST line to remove (inclusive)",
18
18
  });
19
19
 
20
20
  const removeToSchema = Type.String({
21
21
  description:
22
- "Bare 4-char anchor from a read row like `Hasu│content` (the leftmost column), never the row content. Marks the LAST line to remove (inclusive)",
22
+ "Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. Marks the LAST line to remove (inclusive)",
23
23
  });
24
24
 
25
25
  export const editToolSchema = Type.Object(
26
26
  {
27
- path: Type.Optional(
28
- Type.String({
29
- description:
30
- "Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback.",
31
- }),
32
- ),
33
27
  remove_from: removeFromSchema,
34
28
  remove_to: removeToSchema,
35
29
  replacement_lines: replacementLinesSchema,
@@ -38,22 +32,18 @@ export const editToolSchema = Type.Object(
38
32
  );
39
33
 
40
34
  export type ReqParams = {
41
- path: string;
42
35
  remove_from: string;
43
36
  remove_to: string;
44
37
  replacement_lines: string[];
45
38
  };
46
39
 
47
- const ROOT_KS = new Set(["path", "remove_from", "remove_to", "replacement_lines"]);
40
+ const ROOT_KS = new Set(["remove_from", "remove_to", "replacement_lines"]);
48
41
 
49
42
  export function assertReq(request: unknown): asserts request is ReqParams {
50
43
  if (!isRec(request)) {
51
44
  throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
52
45
  }
53
46
  rejectUnknownFields(request, ROOT_KS, "Edit request");
54
- if (typeof request.path !== "string" || request.path.length === 0) {
55
- throw new Error('[E_BAD_SHAPE] Edit request requires a non-empty "path" string.');
56
- }
57
47
  if (
58
48
  typeof request.remove_from !== "string" ||
59
49
  typeof request.remove_to !== "string" ||
@@ -72,6 +62,7 @@ export function normReq(input: unknown): unknown {
72
62
  }
73
63
  const record: Record<string, unknown> = { ...input };
74
64
  normalizeFilePath(record);
65
+ normalizeAnchors(record);
75
66
  return record;
76
67
  }
77
68
 
package/src/read.ts CHANGED
@@ -14,11 +14,11 @@ import { readNormFile, safeSnapId } from "./file-reader";
14
14
  import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
15
15
  import { toCwd } from "./paths";
16
16
  import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
17
- import { recordServedSafe, buildServedMap } from "./served";
18
17
  import { loadP, loadGuide } from "./prompts";
19
18
  import { valAccess } from "./validation";
19
+ import { markServed as markServedScoped } from "./anchor-registry";
20
+ import { buildServedMap } from "./served";
20
21
  import { Text } from "@earendil-works/pi-tui";
21
-
22
22
  const R_DESC = loadP("../prompts/read.md");
23
23
 
24
24
  const R_SNIPPET = loadP("../prompts/read-snippet.md");
@@ -227,6 +227,7 @@ export function regRead(pi: ExtensionAPI): void {
227
227
  const { normalized, fileHashes, hadUtf8DecodeErrors, absolutePath: resolvedPath } = await readNormFile(
228
228
  rawPath, ctx.cwd, { signal, preloadedFile: file, maxLines: MAX_HASH_LINES },
229
229
  );
230
+ const fileLines = splitLines(normalized);
230
231
  const preview = await fmtReadPreview(
231
232
  normalized,
232
233
  {
@@ -236,9 +237,7 @@ export function regRead(pi: ExtensionAPI): void {
236
237
  fileHashes,
237
238
  resolvedPath,
238
239
  );
239
- const fileLines = splitLines(normalized);
240
- const servedMap = buildServedMap(fileHashes, fileLines, preview.servedHashes);
241
- await recordServedSafe(resolvedPath, servedMap, "read", new Set(fileHashes));
240
+ markServedScoped(resolvedPath, buildServedMap(fileHashes, fileLines, preview.servedHashes), new Set(fileHashes));
242
241
  const snapshotId = await safeSnapId(absolutePath, "read");
243
242
  const previewText =
244
243
  hadUtf8DecodeErrors