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.
@@ -2,6 +2,7 @@ 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
4
  import { withLineNumbers } from "./utils";
5
+ import { isDedupRow } from "./replace-response";
5
6
  import { getPreviewInput } from "./payload-contract";
6
7
  export { getPreviewInput };
7
8
 
@@ -19,14 +20,16 @@ export type RRState = {
19
20
  previewGeneration?: number;
20
21
  previewTimer?: ReturnType<typeof setTimeout>;
21
22
  previewAbort?: AbortController;
23
+ resolvedPath?: string;
22
24
  };
23
25
 
24
26
  type DiffRowKind = "added" | "removed" | "context";
25
27
 
26
28
  function diffRowKind(line: string): DiffRowKind {
27
- const stripped = line.replace(/^\s*\d+\s+│\s*/, "");
28
- if (stripped.startsWith("+") && !stripped.startsWith("+++")) return "added";
29
- if (stripped.startsWith("-") && !stripped.startsWith("---")) return "removed";
29
+ const withoutGutter = line.replace(/^\s*\d+\s+│\s*/, "");
30
+ if (isDedupRow(withoutGutter) || isDedupRow(withoutGutter.replace(/^\s*│\s*/, ""))) return "removed";
31
+ if (withoutGutter.startsWith("+") && !withoutGutter.startsWith("+++")) return "added";
32
+ if (withoutGutter.startsWith("-") && !withoutGutter.startsWith("---")) return "removed";
30
33
  return "context";
31
34
  }
32
35
 
@@ -71,7 +74,7 @@ export function fmtCall(
71
74
  toolName = "replace",
72
75
  ): string {
73
76
  const previewPath = state.preview && "path" in state.preview ? state.preview.path : undefined;
74
- const path = args?.path ?? previewPath;
77
+ const path = args?.path ?? state.resolvedPath ?? previewPath;
75
78
  const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : undefined;
76
79
  const pathDisplay =
77
80
  typeof path === "string" && path.length > 0
@@ -237,12 +240,20 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
237
240
 
238
241
  export function makeRenderCall(
239
242
  preview: (args: unknown, cwd: string, signal?: AbortSignal) => Promise<RPreview>,
240
- options: { getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | null; toolName?: string } = {},
243
+ options: {
244
+ getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | null;
245
+ toolName?: string;
246
+ resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string }) => string | undefined;
247
+ } = {},
241
248
  ) {
242
249
  const getInput = options.getInput ?? getPreviewInput;
243
250
  const toolName = options.toolName ?? "replace";
244
251
  return (args: any, theme: CallT, context: any): Text => {
245
252
  const previewInput = getInput(args);
253
+ if (!context.executionStarted && previewInput) {
254
+ const resolved = options.resolveTarget?.(previewInput);
255
+ context.state.resolvedPath = resolved;
256
+ }
246
257
  const cancelPendingPreview = () => {
247
258
  if (context.state.previewTimer) {
248
259
  clearTimeout(context.state.previewTimer);
@@ -137,6 +137,10 @@ export function fmtDedupRow(line: string): string {
137
137
  return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
138
138
  }
139
139
 
140
+ export function isDedupRow(line: string): boolean {
141
+ return line.startsWith(`${DEDUP_ANCHOR}${HASH_SEP}`);
142
+ }
143
+
140
144
  export function isChangeRow(line: string): boolean {
141
145
  return line.startsWith("+") || line.startsWith("-");
142
146
  }
@@ -4,7 +4,10 @@ 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 { recordServed, buildServedMap, servedHashesFromDiff } from "./served";
7
+ import { servedHashesFromDiff, buildServedMap } from "./served";
8
+ import { contentChecksum } from "./hashline/hasher";
9
+ import { hashSource } from "./hashline";
10
+ import { markServed as markServedScoped, freeAnchors, adoptAnchors } from "./anchor-registry";
8
11
  import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
9
12
  import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
10
13
  import { genDiff, genPatch } from "./replace-diff";
@@ -180,11 +183,18 @@ export function regUndo(pi: ExtensionAPI): void {
180
183
 
181
184
  try {
182
185
  const store = await loadHashStore();
183
- persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
184
- const diffHashes = servedHashesFromDiff(undoDiff);
185
186
  const undoLines = splitLines(undo.content);
186
- const servedMap = buildServedMap(undo.hashes, undoLines, diffHashes);
187
- recordServed(store, mutationTargetPath, servedMap, new Set(undo.hashes));
187
+ persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
188
+ freeAnchors(mutationTargetPath);
189
+ adoptAnchors(
190
+ mutationTargetPath,
191
+ new Map(undoLines.map((line, i) => [undo.hashes[i]!, contentChecksum(hashSource(line))])),
192
+ );
193
+ markServedScoped(
194
+ mutationTargetPath,
195
+ buildServedMap(undo.hashes, undoLines, servedHashesFromDiff(undoDiff)),
196
+ new Set(undo.hashes),
197
+ );
188
198
  } catch (error) {
189
199
  console.error("Failed to restore hash store snapshot after undo:", error);
190
200
  }
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,18 +112,17 @@ 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);
132
124
  if (expandedReplacement) {
133
- editWarnings.push('[E_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.');
125
+ editWarnings.push('[W_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.');
134
126
  replacementLines = expandedReplacement;
135
127
  }
136
128
  const edit = resEdit(
@@ -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,30 +257,27 @@ 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 },
287
271
  );
288
272
  const appliedWarnings = boundaryBypass
289
- ? ["[E_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
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/utils.ts CHANGED
@@ -9,11 +9,23 @@ export function normalizeFilePath(record: Record<string, unknown>): void {
9
9
  }
10
10
  }
11
11
 
12
+ export function normalizeAnchors(record: Record<string, unknown>): void {
13
+ if (typeof record.remove_from !== "string" && typeof record.replace_from === "string") {
14
+ record.remove_from = record.replace_from;
15
+ delete record.replace_from;
16
+ }
17
+ if (typeof record.remove_to !== "string" && typeof record.replace_to === "string") {
18
+ record.remove_to = record.replace_to;
19
+ delete record.replace_to;
20
+ }
21
+ }
22
+
12
23
  export function makePrepareArguments(): (args: unknown) => any {
13
24
  return (args) => {
14
25
  if (!isRec(args)) return args;
15
26
  const record = { ...args };
16
27
  normalizeFilePath(record);
28
+ normalizeAnchors(record);
17
29
  return record;
18
30
  };
19
31
  }
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,51 +0,0 @@
1
- import { loadHashStore, findSnapshotPaths, findServedPaths, pickRecentPath } 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: `[E_BAD_SHAPE] Missing "path" resolved to ${single}.` };
17
- }
18
- const sorted = [...matches].sort();
19
- const picked = pickRecentPath(store, sorted);
20
- return { path: picked, warning: `[E_BAD_SHAPE] Missing "path" resolved to ${picked} (picked most recent of ${sorted.length}: ${sorted.join(", ")}).` };
21
- }
22
-
23
- export async function resolveReplacePath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
24
- if (typeof request.path === "string") return undefined;
25
- const from = request.remove_from;
26
- const to = request.remove_to;
27
- if (typeof from !== "string" || typeof to !== "string") return undefined;
28
- const hashes: string[] = [];
29
- for (const ref of [from, to]) {
30
- try {
31
- hashes.push(parseHashRef(ref).hash);
32
- } catch {
33
- return undefined;
34
- }
35
- }
36
- return resolvePathFromHashes(hashes);
37
- }
38
-
39
- export async function resolveInsertPath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
40
- if (typeof request.path === "string") return undefined;
41
- const anchor = request.anchor;
42
- if (typeof anchor !== "string") return undefined;
43
- let hash: string;
44
- try {
45
- const stripped = stripAnchorRow(anchor.trim(), "anchor entry");
46
- hash = parseHashRef(stripped).hash;
47
- } catch {
48
- return undefined;
49
- }
50
- return resolvePathFromHashes([hash]);
51
- }