pi-hashline-edit-pro 3.0.4 → 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.
package/src/replace.ts CHANGED
@@ -3,13 +3,14 @@ import type {
3
3
  ToolDefinition,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import { constants } from "fs";
6
+ import { relative } from "path";
6
7
  import {
7
8
  genDiff,
8
9
  type LineEnding,
9
10
  } from "./replace-diff";
10
11
  import { readNormFile, type NormFile } from "./file-reader";
11
12
  import { editToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
12
- import { decodeStringArray, isRec } from "./utils";
13
+ import { decodeStringArray } from "./utils";
13
14
  import { loadP, loadGuide } from "./prompts";
14
15
  import { type FileIdentity } from "./fs-write";
15
16
  import { applyEdit,
@@ -28,10 +29,11 @@ import {
28
29
  type RRState,
29
30
  } from "./replace-render";
30
31
  import { loadHashStore, type HashStore } from "./hash-store";
31
- import { resolveReplacePath } from "./missing-path";
32
- import { getServed, recordServedSafe } from "./served";
32
+ import { adoptAnchors, servedForPath } from "./anchor-registry";
33
+ import { resolveTarget } from "./fs-write";
34
+ import { toCwd } from "./paths";
33
35
  import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
34
- import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper } from "./edit-common";
36
+ import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTarget } from "./edit-common";
35
37
 
36
38
  export { editToolSchema, type ReqParams, assertReq };
37
39
 
@@ -79,7 +81,7 @@ export interface ExecPipelineOptions {
79
81
  preloadedNorm?: NormFile;
80
82
  }
81
83
 
82
- function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
84
+ export function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
83
85
  const a = hashes.indexOf(from);
84
86
  const b = hashes.indexOf(to);
85
87
  if (a < 0 || b < 0) return undefined;
@@ -87,20 +89,11 @@ function hashSpan(hashes: string[], from: string, to: string): [number, number]
87
89
  }
88
90
  async function noteAnchorError(absolutePath: string, error: unknown, scopeHashes: string[], noPersist?: boolean): Promise<void> {
89
91
  if (noPersist === true) return;
90
- if (error instanceof RangeStaleError) await recordServedSafe(absolutePath, error.rangeServedMap, "range-stale feedback", new Set(scopeHashes));
91
- else if (error instanceof AnchorMismatchError) await recordServedSafe(absolutePath, error.feedbackMap, "anchor-mismatch feedback", new Set(scopeHashes));
92
- }
93
-
94
- function collectRemovedHashes(
95
- edit: HEdit,
96
- originalHashes: string[],
97
- ): Set<string> {
98
- const span = hashSpan(originalHashes, edit.hash_bounds[0].hash, edit.hash_bounds[1].hash);
99
- const removedHashes = new Set<string>();
100
- if (span) {
101
- for (let i = span[0]; i <= span[1]; i++) removedHashes.add(originalHashes[i]!);
92
+ if (error instanceof RangeStaleError) {
93
+ adoptAnchors(absolutePath, error.rangeServedMap);
94
+ } else if (error instanceof AnchorMismatchError) {
95
+ adoptAnchors(absolutePath, error.feedbackMap);
102
96
  }
103
- return removedHashes;
104
97
  }
105
98
 
106
99
  function countLineChanges(
@@ -119,13 +112,12 @@ function countLineChanges(
119
112
  }
120
113
 
121
114
  export async function execPipeline(
115
+ targetPath: string,
122
116
  params: ReqParams,
123
117
  cwd: string,
124
118
  options?: ExecPipelineOptions,
125
119
  ): Promise<PipelineResult> {
126
120
 
127
- const path = params.path;
128
-
129
121
  const editWarnings: string[] = [];
130
122
  let replacementLines = params.replacement_lines;
131
123
  const expandedReplacement = decodeStringArray(replacementLines);
@@ -143,11 +135,13 @@ export async function execPipeline(
143
135
  );
144
136
 
145
137
  const hashStore = options?.store ?? await loadHashStore();
138
+ const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
139
+ const served = servedForPath(preResolvedPath);
146
140
  const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
147
- path, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, preloadedNorm: options?.preloadedNorm },
141
+ targetPath, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, allocation: options?.noPersist ? "shadow" : "real", preloadedNorm: options?.preloadedNorm },
148
142
  );
143
+ const displayPath = relative(cwd, absolutePath).replace(/\\/g, "/") || targetPath;
149
144
 
150
- const served = await getServed(hashStore, absolutePath);
151
145
  let anchorResult: ReturnType<typeof applyEdit>;
152
146
  try {
153
147
  anchorResult = applyEdit(
@@ -155,7 +149,7 @@ export async function execPipeline(
155
149
  edit,
156
150
  options?.signal,
157
151
  originalHashes,
158
- path,
152
+ displayPath,
159
153
  served,
160
154
  options?.skipBoundaryDedup,
161
155
  );
@@ -167,17 +161,12 @@ export async function execPipeline(
167
161
  const result = anchorResult.content;
168
162
  const isNoop = result === originalNormalized;
169
163
 
170
- const noPersist = options?.noPersist;
171
- const removedHashes = isNoop
172
- ? undefined
173
- : collectRemovedHashes(edit, originalHashes);
174
164
  const resultHashes = isNoop
175
165
  ? originalHashes
176
166
  : await lineHashes(result, absolutePath, {
177
167
  content: originalNormalized,
178
168
  hashes: originalHashes,
179
- removedHashes,
180
- }, hashStore, noPersist !== true);
169
+ }, hashStore, false, true);
181
170
  const warnings = [...editWarnings, ...(anchorResult.warnings ?? [])];
182
171
  const { totalAddedLines, totalRemovedLines } = countLineChanges(
183
172
  edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
@@ -187,7 +176,7 @@ export async function execPipeline(
187
176
  const aboveFixes = sortedFixes.filter((fix) => fix.kind === "leading" || fix.kind === "last-new-before");
188
177
  const belowFixes = sortedFixes.filter((fix) => fix.kind === "trailing" || fix.kind === "first-new-after");
189
178
  return {
190
- path,
179
+ path: displayPath,
191
180
  originalNormalized,
192
181
  result,
193
182
  bom,
@@ -230,12 +219,10 @@ export async function compPreview(
230
219
  ): Promise<RPreview> {
231
220
  try {
232
221
  const normalized = normReq(request);
233
- if (isRec(normalized)) {
234
- const resolution = await resolveReplacePath(normalized);
235
- if (resolution) normalized.path = resolution.path;
236
- }
237
222
  assertReq(normalized);
223
+ const targetPath = resolveEditTarget(normalized.remove_from, normalized.remove_to);
238
224
  const pipe = await execPipeline(
225
+ targetPath,
239
226
  normalized,
240
227
  cwd,
241
228
  { accessMode: constants.R_OK, noPersist: true, signal },
@@ -270,17 +257,14 @@ export function buildToolDef(): ToolDef {
270
257
  renderResult: editRenderResultWrapper,
271
258
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
272
259
  const canonical = normReq(params);
273
- const resolution = isRec(canonical) ? await resolveReplacePath(canonical) : undefined;
274
- if (resolution && isRec(canonical)) {
275
- canonical.path = resolution.path;
276
- }
277
260
  assertReq(canonical);
278
261
  const normalizedParams = canonical;
279
- const path = normalizedParams.path;
280
- return queuedEdit(path, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
262
+ const targetPath = resolveEditTarget(normalizedParams.remove_from, normalizedParams.remove_to);
263
+ return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
281
264
  const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
282
265
  const boundaryBypass = consumeBoundaryBypass(mutationTargetPath, noopPayload);
283
266
  const pipe = await execPipeline(
267
+ targetPath,
284
268
  normalizedParams,
285
269
  ctx.cwd,
286
270
  { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
@@ -289,11 +273,11 @@ export function buildToolDef(): ToolDef {
289
273
  ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
290
274
  : [];
291
275
  return commitEdit(pipe, {
292
- path,
276
+ path: pipe.path,
293
277
  absolutePath,
294
278
  mutationTargetPath,
279
+ editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
295
280
  signal,
296
- prefixWarnings: resolution ? [resolution.warning] : [],
297
281
  appliedWarnings,
298
282
  onApplied: () => clearBoundaryBypass(mutationTargetPath),
299
283
  onNoopDedup: () => markBoundaryNoop(mutationTargetPath, noopPayload),
package/src/served.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { loadHashStore, parseStoredServed, STORE_NOT_OPEN_MESSAGE, withStore, type HashStore } from "./hash-store";
2
- import { touchSession } from "./hash-store/cache";
3
- import { withBusyRetry } from "./hash-store/retry";
4
1
  import { HASH_CLASS } from "./hashline/alphabet";
2
+ import { hashSource } from "./hashline";
5
3
  import { contentChecksum } from "./hashline/hasher";
6
4
 
7
5
  const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
@@ -15,125 +13,20 @@ export function servedHashesFromDiff(diff: string): string[] {
15
13
  return hashes;
16
14
  }
17
15
 
18
- export function servedMapFromDiff(diff: string): Map<string, string> {
19
- const map = new Map<string, string>();
20
- for (const line of diff.split("\n")) {
21
- const match = SERVED_DIFF_ROW_RE.exec(line);
22
- if (!match) continue;
23
- const hash = match[1]!;
24
- const content = line.slice(match[0].length);
25
- map.set(hash, contentChecksum(content));
26
- }
27
- return map;
28
- }
29
-
30
- export function buildServedMap(fileHashes: string[], fileLines: string[], wantedHashes: string[]): Map<string, string> {
16
+ export function buildServedMap(
17
+ fileHashes: string[],
18
+ fileLines: string[],
19
+ wantedHashes: string[],
20
+ ): Array<[string, string]> {
31
21
  const index = new Map<string, number>();
32
- for (let i = 0; i < fileHashes.length; i++) index.set(fileHashes[i]!, i);
33
- const map = new Map<string, string>();
34
- for (const h of wantedHashes) {
35
- const idx = index.get(h);
36
- if (idx !== undefined) map.set(h, contentChecksum(fileLines[idx]!));
37
- }
38
- return map;
39
- }
40
-
41
- export function getServed(store: HashStore, path: string): Map<string, string> | undefined {
42
- const row = withBusyRetry(() => store.stmts.servedGet(path));
43
- const parsed = parseStoredServed(row, () => store.stmts.servedDelete(path));
44
- if (!parsed) return undefined;
45
- return parsed;
46
- }
47
-
48
- function computeUpdate(
49
- store: HashStore,
50
- path: string,
51
- entries: Map<string, string>,
52
- scope?: ReadonlySet<string>,
53
- ): Map<string, string> | undefined {
54
- const existing = getServed(store, path);
55
- if (!existing && entries.size === 0 && !scope) return undefined;
56
- const map = existing ? new Map(existing) : new Map<string, string>();
57
- let changed = false;
58
- if (scope) {
59
- for (const hash of [...map.keys()]) {
60
- if (!scope.has(hash)) {
61
- map.delete(hash);
62
- changed = true;
63
- }
64
- }
65
- }
66
- for (const [hash, content] of entries) {
67
- const prev = map.get(hash);
68
- if (prev !== content) {
69
- map.set(hash, content);
70
- changed = true;
71
- }
22
+ for (let i = 0; i < fileHashes.length; i++) {
23
+ const existing = index.get(fileHashes[i]!);
24
+ if (existing === undefined) index.set(fileHashes[i]!, i);
72
25
  }
73
- if (!changed && existing) return undefined;
74
- if (map.size === 0 && (!existing || existing.size === 0)) return undefined;
75
- if (!changed) return existing;
76
- return map;
77
- }
78
-
79
- export function recordServed(
80
- store: HashStore,
81
- path: string,
82
- entries: Map<string, string>,
83
- scope?: ReadonlySet<string>,
84
- ): void {
85
- touchSession(path);
86
- try {
87
- withStore(() => {
88
- const map = computeUpdate(store, path, entries, scope);
89
- if (!map) return;
90
- const obj = Object.fromEntries(map);
91
- store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
92
- });
93
- return;
94
- } catch (error) {
95
- if (!(error instanceof Error && error.message === STORE_NOT_OPEN_MESSAGE)) throw error;
26
+ const entries: Array<[string, string]> = [];
27
+ for (const hash of wantedHashes) {
28
+ const idx = index.get(hash);
29
+ if (idx !== undefined) entries.push([hash, contentChecksum(hashSource(fileLines[idx]!))]);
96
30
  }
97
- const map = computeUpdate(store, path, entries, scope);
98
- if (!map) return;
99
- const obj = Object.fromEntries(map);
100
- store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
101
- }
102
-
103
- export function recordServedDiff(
104
- store: HashStore,
105
- path: string,
106
- diff: string,
107
- scope?: ReadonlySet<string>,
108
- ): void {
109
- recordServed(store, path, servedMapFromDiff(diff), scope);
110
- }
111
-
112
- export function clearServed(store: HashStore, path: string): void {
113
- store.stmts.servedDelete(path);
114
- }
115
-
116
- export async function recordServedSafe(
117
- path: string,
118
- entries: Map<string, string>,
119
- context: string,
120
- scope?: ReadonlySet<string>,
121
- ): Promise<void> {
122
- if (entries.size === 0 && !scope) return;
123
- try {
124
- const store = await loadHashStore();
125
- recordServed(store, path, entries, scope);
126
- } catch (error) {
127
- console.error(`Failed to record served state (${context}):`, error);
128
- }
129
- }
130
-
131
- export async function recordServedDiffSafe(
132
- path: string,
133
- diff: string,
134
- context: string,
135
- scope?: ReadonlySet<string>,
136
- ): Promise<void> {
137
- if (!diff) return;
138
- await recordServedSafe(path, servedMapFromDiff(diff), context, scope);
31
+ return entries;
139
32
  }
package/src/write-hook.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { HASH_CLASS } from "./hashline/alphabet";
3
3
  import { HASH_SEP } from "./hashline/hash";
4
- import { loadHashStore } from "./hash-store";
5
- import { getServed } from "./served";
4
+ import { ownersForPath } from "./anchor-registry";
6
5
  import { resolveInCwd } from "./fs-write";
7
6
  import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
8
7
 
@@ -28,8 +27,7 @@ export async function servedHashEchoDenial(rawPath: string, content: string, cwd
28
27
  abortIf(signal);
29
28
  const { resolved } = await resolveInCwd(rawPath, cwd);
30
29
  abortIf(signal);
31
- const store = await loadHashStore();
32
- const served = getServed(store, resolved);
30
+ const served = ownersForPath(resolved);
33
31
  if (!served || served.size === 0) return undefined;
34
32
  const echo = findServedHashEcho(content, served);
35
33
  if (!echo) return undefined;
@@ -1,54 +0,0 @@
1
- import { loadHashStore, findSnapshotPaths, findServedPaths, rankRecentPaths } from "./hash-store";
2
- import { parseHashRef } from "./hashline/parse";
3
- import { stripAnchorRow } from "./hashline/resolve";
4
-
5
- export async function resolvePathFromHashes(hashes: string[]): Promise<{ path: string; warning: string } | undefined> {
6
- let store;
7
- try {
8
- store = await loadHashStore();
9
- } catch {
10
- return undefined;
11
- }
12
- const matches = [...new Set([...findSnapshotPaths(store, hashes), ...findServedPaths(store, hashes)])];
13
- if (matches.length === 0) return undefined;
14
- if (matches.length === 1) {
15
- const single = matches[0]!;
16
- return { path: single, warning: `[W_BAD_SHAPE] Missing "path" resolved to ${single}.` };
17
- }
18
- const ranked = rankRecentPaths(store, [...matches]);
19
- const picked = ranked[0]!;
20
- const shown = ranked.slice(0, 3);
21
- const hidden = ranked.length - shown.length;
22
- const listed = hidden > 0 ? `${shown.join(", ")}, ... (+${hidden} more)` : shown.join(", ");
23
- return { path: picked, warning: `[W_BAD_SHAPE] Missing "path" resolved to ${picked} (picked most recent of ${ranked.length}: ${listed}).` };
24
- }
25
-
26
- export async function resolveReplacePath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
27
- if (typeof request.path === "string") return undefined;
28
- const from = request.remove_from;
29
- const to = request.remove_to;
30
- if (typeof from !== "string" || typeof to !== "string") return undefined;
31
- const hashes: string[] = [];
32
- for (const ref of [from, to]) {
33
- try {
34
- hashes.push(parseHashRef(ref).hash);
35
- } catch {
36
- return undefined;
37
- }
38
- }
39
- return resolvePathFromHashes(hashes);
40
- }
41
-
42
- export async function resolveInsertPath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
43
- if (typeof request.path === "string") return undefined;
44
- const anchor = request.anchor;
45
- if (typeof anchor !== "string") return undefined;
46
- let hash: string;
47
- try {
48
- const stripped = stripAnchorRow(anchor.trim(), "anchor entry");
49
- hash = parseHashRef(stripped).hash;
50
- } catch {
51
- return undefined;
52
- }
53
- return resolvePathFromHashes([hash]);
54
- }