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.
- package/README.md +118 -132
- package/index.ts +22 -11
- package/package.json +1 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +1 -2
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +1 -1
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +0 -1
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/anchor-registry.ts +605 -0
- package/src/commit.ts +21 -7
- package/src/constants.ts +4 -1
- package/src/edit-common.ts +45 -8
- package/src/file-reader.ts +4 -1
- package/src/grep.ts +26 -8
- package/src/hash-store/cache.ts +0 -4
- package/src/hash-store.ts +50 -138
- package/src/hashline/anchor-table.json +1 -1
- package/src/hashline/apply.ts +4 -1
- package/src/hashline/hash.ts +12 -175
- package/src/hashline/parse.ts +4 -4
- package/src/hashline/resolve.ts +15 -47
- package/src/insert.ts +17 -32
- package/src/paths.ts +4 -0
- package/src/payload-contract.ts +5 -14
- package/src/read.ts +4 -5
- package/src/replace-render.ts +16 -5
- package/src/replace-response.ts +4 -0
- package/src/replace-undo.ts +15 -5
- package/src/replace.ts +28 -44
- package/src/served.ts +14 -121
- package/src/utils.ts +12 -0
- package/src/write-hook.ts +2 -4
- package/src/missing-path.ts +0 -51
package/src/hashline/apply.ts
CHANGED
|
@@ -167,7 +167,10 @@ export function applyEdit(
|
|
|
167
167
|
abortIf(signal);
|
|
168
168
|
|
|
169
169
|
const lineIndex = buildIdx(content);
|
|
170
|
-
|
|
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);
|
package/src/hashline/hash.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { splitLines, truncateToBytes, getCached } from "../utils";
|
|
2
2
|
import { MAX_HASH_SOURCE_BYTES } from "../constants";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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,
|
|
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 =
|
|
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[];
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
}
|
package/src/hashline/parse.ts
CHANGED
|
@@ -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
|
-
'[
|
|
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
|
-
'[
|
|
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
|
|
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
|
-
"[
|
|
93
|
+
"[W_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
return out;
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(["
|
|
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
|
|
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,
|
|
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
|
|
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('[
|
|
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
|
|
177
|
+
const targetPath = resolveEditTarget(req.anchor);
|
|
193
178
|
const { ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor);
|
|
194
|
-
return queuedEdit(
|
|
195
|
-
const preload = await readNormFile(
|
|
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: [...
|
|
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
|
}
|
package/src/payload-contract.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(["
|
|
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
|
-
|
|
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
|