pi-hashline-edit-pro 4.2.11 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/batch.ts CHANGED
@@ -17,12 +17,12 @@ import {
17
17
  type PlannedEdit,
18
18
  } from "./hashline";
19
19
  import { boundaryDedupNoopWarning, boundaryDedupWarning, dedupRowsFromFixes } from "./commit";
20
- import { adoptAnchors, markServed, servedForPath } from "./anchor-registry";
20
+ import { adoptAnchors, servedForPath } from "./anchor-registry";
21
21
  import { restoreEndings, stripBOM, toLF, type LineEnding } from "./normalize";
22
22
  import { assertInsertReq, assertReq, normReq } from "./payload-contract";
23
23
  import { saveUndo } from "./replace-undo";
24
24
  import { buildChanged, buildNoop, type RMetrics, type TResult } from "./replace-response";
25
- import { buildServedMap, servedHashesFromDiff } from "./served";
25
+ import { serveRows, servedHashesFromDiff } from "./served";
26
26
  import { abortIf, assertLineLimit, errCode, isRec, splitLines } from "./utils";
27
27
  import { MAX_BYTES } from "./constants";
28
28
 
@@ -750,7 +750,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
750
750
  changed.details.diff = `${header}\n${changed.details.diff}`;
751
751
  changed.details.diffLineNumbers?.unshift(undefined);
752
752
  try {
753
- markServed(runtime.target, buildServedMap(resultHashes, splitLines(composed), servedHashesFromDiff(changed.details.diff)), new Set(resultHashes));
753
+ serveRows(runtime.target, resultHashes, splitLines(composed), servedHashesFromDiff(changed.details.diff));
754
754
  } catch (error) {
755
755
  console.error("Failed to mark batch diff served:", error);
756
756
  }
package/src/commit.ts CHANGED
@@ -8,11 +8,10 @@ import { saveUndo } from "./replace-undo";
8
8
  import { getDiffContextLines } from "./config";
9
9
  import { safeSnapId } from "./file-reader";
10
10
  import { writeAtomic } from "./fs-write";
11
- import { servedHashesFromDiff, buildServedMap } from "./served";
11
+ import { servedHashesFromDiff, serveRows } from "./served";
12
12
  import { lineHashes, type AutoFix } from "./hashline";
13
- import { hashSpan } from "./replace";
13
+ import { spanForEdit } from "./replace";
14
14
  import { restoreEndings, stripBOM, toLF } from "./normalize";
15
- import { markServed as markServedScoped } from "./anchor-registry";
16
15
  export interface CommitMeta {
17
16
  editAnchors?: [string, string];
18
17
  path: string;
@@ -131,18 +130,14 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
131
130
  removedLines: pipe.totalRemovedLines,
132
131
  };
133
132
 
134
- const span = meta.editAnchors ? hashSpan(pipe.originalHashes, meta.editAnchors[0], meta.editAnchors[1]) : undefined;
135
- const resultCount = splitLines(pipe.result).length;
136
- const replacementCount = span ? resultCount - (pipe.originalHashes.length - (span[1] - span[0] + 1)) : 0;
133
+ const span = meta.editAnchors ? spanForEdit(pipe.originalHashes, meta.editAnchors[0], meta.editAnchors[1], pipe.result) : undefined;
137
134
  let resultHashes: string[];
138
135
  try {
139
- resultHashes = pipe.result === pipe.originalNormalized
140
- ? pipe.originalHashes
141
- : await lineHashes(pipe.result, mutationTargetPath, {
142
- content: pipe.originalNormalized,
143
- hashes: pipe.originalHashes,
144
- spans: span ? [{ start: span[0], end: span[1], replacementCount }] : undefined,
145
- });
136
+ resultHashes = await lineHashes(pipe.result, mutationTargetPath, {
137
+ content: pipe.originalNormalized,
138
+ hashes: pipe.originalHashes,
139
+ spans: span ? [span] : undefined,
140
+ });
146
141
  } catch (error) {
147
142
  const detail = error instanceof Error ? error.message : String(error);
148
143
  throw new Error(`${detail} File was written; anchor finalization failed. One undo reverts. Call read for fresh anchors.`);
@@ -158,15 +153,11 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
158
153
  editMeta,
159
154
  boundaryDedupAbove: pipe.boundaryDedupAbove,
160
155
  boundaryDedupBelow: pipe.boundaryDedupBelow,
161
- ...(span ? { spans: [{ start: span[0], end: span[1], replacementCount }] } : {}),
156
+ ...(span ? { spans: [span] } : {}),
162
157
  };
163
158
  const changed = buildChanged(successInput, meta.verb, await getDiffContextLines());
164
159
  if (changed.details.diff) {
165
- markServedScoped(
166
- mutationTargetPath,
167
- buildServedMap(resultHashes, splitLines(pipe.result), servedHashesFromDiff(changed.details.diff)),
168
- new Set(resultHashes),
169
- );
160
+ serveRows(mutationTargetPath, resultHashes, splitLines(pipe.result), servedHashesFromDiff(changed.details.diff));
170
161
  }
171
162
  return changed;
172
163
  }
package/src/config-ui.ts CHANGED
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { readConfig, type Config } from "./config";
4
4
 
5
- export type ConfigToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode" | "diffContextLines";
5
+ export type ConfigToggleKey = "autoRead" | "autoReadAll" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode" | "diffContextLines";
6
6
 
7
7
  export interface ConfigRow {
8
8
  key: ConfigToggleKey;
@@ -18,6 +18,7 @@ export interface ConfigRow {
18
18
  export function configRows(config: Config): ConfigRow[] {
19
19
  return [
20
20
  { key: "autoRead", label: "Auto-read", hint: "Anchors after write + post-edit diffs", enabled: config.autoRead !== false },
21
+ { key: "autoReadAll", label: "Auto-read all", hint: "Attach every non-ignored file with anchors on the first turn", enabled: config.autoReadAll === true },
21
22
  { key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
22
23
  { key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
23
24
  { key: "requirePath", label: "Require path", hint: "replace + insert need path (RPC visibility)", enabled: config.requirePath === true },
package/src/config.ts CHANGED
@@ -12,6 +12,7 @@ export const MAX_DIFF_CONTEXT_LINES = 10;
12
12
  export interface Config {
13
13
  autoRead: boolean;
14
14
  anchorGrepEnabled: boolean;
15
+ autoReadAll?: boolean;
15
16
  requirePath?: boolean;
16
17
  strictInput?: boolean;
17
18
  boundaryDedupMode?: BoundaryDedupMode;
@@ -21,6 +22,7 @@ export interface Config {
21
22
  const DEFAULT_CONFIG: Config = {
22
23
  autoRead: true,
23
24
  anchorGrepEnabled: true,
25
+ autoReadAll: false,
24
26
  requirePath: false,
25
27
  strictInput: false,
26
28
  boundaryDedupMode: "on",
@@ -50,15 +52,17 @@ function parseConfig(content: string): Config {
50
52
  throw new Error("config.json must be an object with a boolean autoRead field");
51
53
  }
52
54
  const autoRead = parsed.autoRead;
53
- const anchorGrepEnabled = isRec(parsed) ? parsed.anchorGrepEnabled : undefined;
54
- const requirePath = isRec(parsed) ? parsed.requirePath : undefined;
55
- const strictInput = isRec(parsed) ? parsed.strictInput : undefined;
56
- const boundaryDedupMode = isRec(parsed) ? parsed.boundaryDedupMode : undefined;
57
- const legacyBoundaryDedup = isRec(parsed) ? parsed.boundaryDedupEnabled : undefined;
58
- const diffContextLines = isRec(parsed) ? parsed.diffContextLines : undefined;
55
+ const anchorGrepEnabled = parsed.anchorGrepEnabled;
56
+ const autoReadAll = parsed.autoReadAll;
57
+ const requirePath = parsed.requirePath;
58
+ const strictInput = parsed.strictInput;
59
+ const boundaryDedupMode = parsed.boundaryDedupMode;
60
+ const legacyBoundaryDedup = parsed.boundaryDedupEnabled;
61
+ const diffContextLines = parsed.diffContextLines;
59
62
  return {
60
63
  autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
61
64
  anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
65
+ autoReadAll: typeof autoReadAll === "boolean" ? autoReadAll : DEFAULT_CONFIG.autoReadAll,
62
66
  requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
63
67
  strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
64
68
  boundaryDedupMode: parseBoundaryDedupMode(boundaryDedupMode, legacyBoundaryDedup),
@@ -92,17 +96,23 @@ export async function readConfig(): Promise<Config> {
92
96
  export async function readConfigWithStatus(): Promise<{ config: Config; corrupted: boolean }> {
93
97
  return loadConfigFile();
94
98
  }
95
- const CONFIG_LOCK_RETRIES = 80;
96
99
  const CONFIG_LOCK_DELAY_MS = 25;
97
100
  const CONFIG_LOCK_STALE_MS = 5000;
98
- async function acquireConfigLock(lockPath: string): Promise<void> {
101
+ const CONFIG_LOCK_RETRIES = Math.ceil(CONFIG_LOCK_STALE_MS / CONFIG_LOCK_DELAY_MS) * 2;
102
+
103
+ interface ConfigLock {
104
+ path: string;
105
+ dev: number;
106
+ ino: number;
107
+ }
108
+
109
+ async function acquireConfigLock(lockPath: string): Promise<ConfigLock> {
99
110
  try {
100
111
  await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
101
112
  } catch { }
102
113
  for (let attempt = 0; attempt < CONFIG_LOCK_RETRIES; attempt++) {
103
114
  try {
104
115
  await mkdir(lockPath, { mode: 0o700 });
105
- return;
106
116
  } catch (error) {
107
117
  if (errCode(error) === "ENOENT") {
108
118
  try {
@@ -119,26 +129,44 @@ async function acquireConfigLock(lockPath: string): Promise<void> {
119
129
  }
120
130
  } catch { }
121
131
  await new Promise<void>((r) => setTimeout(r, CONFIG_LOCK_DELAY_MS));
132
+ continue;
133
+ }
134
+ try {
135
+ const st = await stat(lockPath);
136
+ return { path: lockPath, dev: st.dev, ino: st.ino };
137
+ } catch (error) {
138
+ if (errCode(error) !== "ENOENT") {
139
+ try {
140
+ await rm(lockPath, { recursive: true, force: true });
141
+ } catch { }
142
+ }
143
+ continue;
122
144
  }
123
145
  }
124
146
  throw new Error(`[E_ACCESS] Could not acquire config lock: ${lockPath}`);
125
147
  }
126
- async function releaseConfigLock(lockPath: string): Promise<void> {
148
+ async function releaseConfigLock(lock: ConfigLock): Promise<void> {
149
+ try {
150
+ const st = await stat(lock.path);
151
+ if (st.dev !== lock.dev || st.ino !== lock.ino) return;
152
+ } catch {
153
+ return;
154
+ }
127
155
  try {
128
- await rm(lockPath, { recursive: true, force: true });
156
+ await rm(lock.path, { recursive: true, force: true });
129
157
  } catch { }
130
158
  }
131
159
  export async function updateConfig(mut: (config: Config) => void): Promise<Config> {
132
160
  const cfgPath = configPath();
133
161
  const lockPath = `${cfgPath}.lock`;
134
- await acquireConfigLock(lockPath);
162
+ const lock = await acquireConfigLock(lockPath);
135
163
  try {
136
164
  const config = await readConfig();
137
165
  mut(config);
138
166
  await writeConfig(config);
139
167
  return config;
140
168
  } finally {
141
- await releaseConfigLock(lockPath);
169
+ await releaseConfigLock(lock);
142
170
  }
143
171
  }
144
172
  export async function writeConfig(config: Config): Promise<void> {
@@ -146,22 +174,17 @@ export async function writeConfig(config: Config): Promise<void> {
146
174
  }
147
175
 
148
176
 
149
- export async function toggleAutoRead(): Promise<boolean> {
150
- const config = await updateConfig((c) => { c.autoRead = !c.autoRead; });
151
- return config.autoRead;
152
- }
153
- export async function toggleAnchorGrep(): Promise<boolean> {
154
- const config = await updateConfig((c) => { c.anchorGrepEnabled = !c.anchorGrepEnabled; });
155
- return config.anchorGrepEnabled;
156
- }
157
- export async function toggleRequirePath(): Promise<boolean> {
158
- const config = await updateConfig((c) => { c.requirePath = !c.requirePath; });
159
- return config.requirePath === true;
160
- }
161
- export async function toggleStrictInput(): Promise<boolean> {
162
- const config = await updateConfig((c) => { c.strictInput = !(c.strictInput === true); });
163
- return config.strictInput === true;
164
- }
177
+ type ToggleKey = "autoRead" | "anchorGrepEnabled" | "autoReadAll" | "requirePath" | "strictInput";
178
+
179
+ async function toggleFlag(key: ToggleKey): Promise<boolean> {
180
+ const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
181
+ return config[key] === true;
182
+ }
183
+ export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
184
+ export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
185
+ export const toggleAutoReadAll = (): Promise<boolean> => toggleFlag("autoReadAll");
186
+ export const toggleRequirePath = (): Promise<boolean> => toggleFlag("requirePath");
187
+ export const toggleStrictInput = (): Promise<boolean> => toggleFlag("strictInput");
165
188
  export async function cycleBoundaryDedupMode(): Promise<BoundaryDedupMode> {
166
189
  let next: BoundaryDedupMode = "on";
167
190
  await updateConfig((c) => {
package/src/constants.ts CHANGED
@@ -20,3 +20,9 @@ export const ANCHOR_POOL_EXHAUSTED_PREFIX =
20
20
  "[E_FILE_TOO_LARGE] The session's anchor pool is exhausted";
21
21
 
22
22
  export const DEDUP_ANCHOR = "dedup";
23
+
24
+ export const AUTO_READ_ALL_CUSTOM_TYPE = "hashline-auto-read-all";
25
+ export const AUTO_READ_ALL_MAX_FILES = 500;
26
+ export const AUTO_READ_ALL_MAX_FILE_BYTES = 200_000;
27
+ export const AUTO_READ_ALL_MIN_BUDGET_BYTES = 200_000;
28
+ export const AUTO_READ_ALL_MAX_BUDGET_BYTES = 2_000_000;
package/src/fs-write.ts CHANGED
@@ -21,7 +21,7 @@ export interface FileIdentity {
21
21
  }
22
22
 
23
23
  function sameIdentity(
24
- actual: Pick<Awaited<ReturnType<typeof stat>>, "dev" | "ino">,
24
+ actual: FileIdentity,
25
25
  expected: FileIdentity,
26
26
  ): boolean {
27
27
  return actual.dev === expected.dev && actual.ino === expected.ino;
package/src/grep.ts CHANGED
@@ -12,8 +12,8 @@ import { toCwd } from "./paths";
12
12
  import { loadP, loadGuide } from "./prompts";
13
13
  import { normReq } from "./payload-contract";
14
14
  import { abortIf, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
15
- import { markServed as markServedScoped, withAnchorSession } from "./anchor-registry";
16
- import { buildServedMap } from "./served";
15
+ import { withAnchorSession } from "./anchor-registry";
16
+ import { serveRows } from "./served";
17
17
  import { Text } from "@earendil-works/pi-tui";
18
18
  import { expandHint, getResultText, reuseText, type CallT, type FgT } from "./replace-render";
19
19
  const GREP_KS = new Set(["pattern", "path", "glob", "context", "ignoreCase", "literal", "limit"]);
@@ -281,11 +281,8 @@ function makeHitFromIndices(
281
281
  }
282
282
 
283
283
  let cachedRgPath: string | undefined;
284
- export function clearRgPathCache(): void {
285
- cachedRgPath = undefined;
286
- }
287
284
 
288
- async function resolveRgPath(): Promise<string> {
285
+ export async function resolveRgPath(): Promise<string> {
289
286
  if (cachedRgPath !== undefined) return cachedRgPath;
290
287
  try {
291
288
  const r = spawnSync("rg", ["--version"], { stdio: "pipe" });
@@ -572,6 +569,12 @@ export function regGrep(pi: ExtensionAPI): void {
572
569
  }
573
570
  const globRoot = baseStat.isFile() ? dirname(base) : base;
574
571
  const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
572
+ const matchesGlob = (absPath: string): boolean => {
573
+ if (globRegex === undefined) return true;
574
+ const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
575
+ const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
576
+ return globRegex.test(globPath) || globRegex.test(displayPath);
577
+ };
575
578
  const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
576
579
  const rgPath = await resolveRgPath();
577
580
  const hits: FileHit[] = [];
@@ -607,11 +610,7 @@ export function regGrep(pi: ExtensionAPI): void {
607
610
  const sortedNums = [...allNums].sort((a, b) => a - b);
608
611
  const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
609
612
  if (countOnly) {
610
- if (globRegex) {
611
- const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
612
- const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
613
- if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
614
- }
613
+ if (!matchesGlob(absPath)) continue;
615
614
  const norm = await readGrepFileShadow(absPath);
616
615
  if (!norm) continue;
617
616
  const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
@@ -633,15 +632,10 @@ export function regGrep(pi: ExtensionAPI): void {
633
632
  limitTruncated = true;
634
633
  break;
635
634
  }
636
- if (globRegex) {
637
- const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
638
- const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
639
- if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
640
- }
635
+ if (!matchesGlob(absPath)) continue;
641
636
  const norm = await readGrepFile(absPath);
642
637
  if (!norm) continue;
643
638
  const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
644
- if (!hit) continue;
645
639
  const display = displayRowsForHit(hit);
646
640
  const keptRows: string[] = [];
647
641
  const keptHashes: string[] = [];
@@ -677,7 +671,7 @@ export function regGrep(pi: ExtensionAPI): void {
677
671
  }
678
672
  hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
679
673
  for (const hit of hits) {
680
- markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
674
+ serveRows(hit.path, hit.fileHashes, hit.fileLines, hit.hashes);
681
675
  }
682
676
  const blocks = hits
683
677
  .map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
@@ -16,21 +16,3 @@ export function cacheSnapshot(path: string, checksum: string, lineCount: number,
16
16
  if (oldest !== undefined) snapshotCache.delete(oldest);
17
17
  }
18
18
  }
19
-
20
- let sessionSeq = 0;
21
- export const sessionTouched = new Map<string, number>();
22
-
23
- export function touchSession(path: string): void {
24
- sessionSeq += 1;
25
- sessionTouched.delete(path);
26
- sessionTouched.set(path, sessionSeq);
27
- }
28
-
29
- export function clearSession(): void {
30
- sessionTouched.clear();
31
- sessionSeq = 0;
32
- }
33
-
34
- export function forgetSession(path: string): void {
35
- sessionTouched.delete(path);
36
- }
@@ -42,7 +42,3 @@ export function retriedWrite(stmt: { run(...params: (string | number | null)[]):
42
42
  withBusyRetry(() => { stmt.run(...params); });
43
43
  };
44
44
  }
45
-
46
- export async function openDbWithBusyRetryAsync<T>(fn: () => T): Promise<T> {
47
- return withBusyRetryAsync(fn);
48
- }
@@ -9,15 +9,6 @@ export function isValidHashList(value: unknown): value is string[] {
9
9
  return true;
10
10
  }
11
11
 
12
- export function isValidServedMap(value: unknown): value is Record<string, string> {
13
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
14
- for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
15
- if (typeof k !== "string" || !HASH_RE.test(k)) return false;
16
- if (typeof v !== "string") return false;
17
- }
18
- return true;
19
- }
20
-
21
12
  export function parseHashList(raw: string, onInvalid: () => void, context?: string): string[] | undefined {
22
13
  let parsed: unknown;
23
14
  try {
@@ -35,33 +26,11 @@ export function parseHashList(raw: string, onInvalid: () => void, context?: stri
35
26
  return parsed;
36
27
  }
37
28
 
38
- export function parseServedMap(raw: string, onInvalid: () => void, context?: string): Map<string, string> | undefined {
39
- let parsed: unknown;
40
- try {
41
- parsed = JSON.parse(raw);
42
- } catch (error) {
43
- console.error(`[parseServedMap]${context ? ` ${context}:` : ""} failed to parse stored served JSON:`, error);
44
- onInvalid();
45
- return undefined;
46
- }
47
- if (!isValidServedMap(parsed)) {
48
- console.error(`[parseServedMap]${context ? ` ${context}:` : ""} stored served did not pass validation:`, (() => { try { return JSON.stringify(parsed)?.slice(0, 500) ?? String(parsed).slice(0, 500); } catch { return String(parsed).slice(0, 500); } })());
49
- onInvalid();
50
- return undefined;
51
- }
52
- return new Map(Object.entries(parsed as Record<string, string>));
53
- }
54
-
55
29
  export function parseStoredHashes(row: Record<string, unknown> | undefined, onInvalid: () => void): string[] | undefined {
56
30
  if (!row) return undefined;
57
31
  return parseHashList(row.hashes as string, onInvalid);
58
32
  }
59
33
 
60
- export function parseStoredServed(row: Record<string, unknown> | undefined, onInvalid: () => void): Map<string, string> | undefined {
61
- if (!row) return undefined;
62
- return parseServedMap(row.hashes as string, onInvalid);
63
- }
64
-
65
34
  export function isValidSnapshot(value: unknown): value is { content: string; hashes: string[] } {
66
35
  if (typeof value !== "object" || value === null) return false;
67
36
  const v = value as Record<string, unknown>;
package/src/hash-store.ts CHANGED
@@ -7,22 +7,19 @@ import { initHasher, contentChecksum } from "./hashline/hasher";
7
7
  import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
8
8
  import {
9
9
  isValidHashList,
10
- isValidServedMap,
11
10
  parseStoredHashes,
12
- parseStoredServed,
13
11
  isValidSnapshot,
14
12
  isCorruptionError,
15
13
  parseHashList,
16
- parseServedMap,
17
14
  } from "./hash-store/validation";
18
15
  import {
19
16
  withBusyRetry,
20
17
  retriedWrite,
21
- openDbWithBusyRetryAsync,
18
+ withBusyRetryAsync,
22
19
  } from "./hash-store/retry";
23
- import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT, touchSession, clearSession, forgetSession } from "./hash-store/cache";
20
+ import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT } from "./hash-store/cache";
24
21
 
25
- export { isValidHashList, isValidServedMap, parseHashList, parseServedMap, parseStoredHashes, parseStoredServed, isCorruptionError };
22
+ export { isValidHashList, parseHashList, parseStoredHashes, isCorruptionError };
26
23
  export { SNAPSHOT_CACHE_LIMIT };
27
24
  export const STORE_NOT_OPEN_MESSAGE = "Hash store is not open; transactional update aborted";
28
25
 
@@ -283,19 +280,19 @@ async function openStore(storePath: string): Promise<HashStore> {
283
280
  let existed = existsSync(storePath);
284
281
  let opened: { db: RawDb; stmts: Prepared };
285
282
  try {
286
- opened = await openDbWithBusyRetryAsync(() => openDb(storePath));
283
+ opened = await withBusyRetryAsync(() => openDb(storePath));
287
284
  } catch (error) {
288
285
  if (!isCorruptionError(error)) throw error;
289
286
  console.error("Hash store failed to open, rebuilding:", error);
290
287
  await quarantineStore(storePath);
291
288
  existed = false;
292
- opened = await openDbWithBusyRetryAsync(() => openDb(storePath));
289
+ opened = await withBusyRetryAsync(() => openDb(storePath));
293
290
  }
294
291
  if (!isHealthy(opened.db)) {
295
292
  shutdownDb(opened.db);
296
293
  await quarantineStore(storePath);
297
294
  existed = false;
298
- opened = await openDbWithBusyRetryAsync(() => openDb(storePath));
295
+ opened = await withBusyRetryAsync(() => openDb(storePath));
299
296
  }
300
297
  const { db, stmts } = opened;
301
298
  try {
@@ -367,25 +364,28 @@ export function shutdownHashStore(): void {
367
364
  cachedDb = null;
368
365
  }
369
366
  snapshotCache.clear();
370
- clearSession();
371
367
  }
372
368
 
373
- export function withStore(fn: () => void): void {
374
- if (!cachedDb || !cachedDb.db.isOpen) {
375
- throw new Error(STORE_NOT_OPEN_MESSAGE);
376
- }
369
+ function withTransaction(db: RawDb, fn: () => void): void {
377
370
  withBusyRetry(() => {
378
- cachedDb!.db.exec("BEGIN IMMEDIATE");
371
+ db.exec("BEGIN IMMEDIATE");
379
372
  try {
380
373
  fn();
381
- cachedDb!.db.exec("COMMIT");
374
+ db.exec("COMMIT");
382
375
  } catch (e) {
383
- try { cachedDb!.db.exec("ROLLBACK"); } catch {}
376
+ try { db.exec("ROLLBACK"); } catch {}
384
377
  throw e;
385
378
  }
386
379
  });
387
380
  }
388
381
 
382
+ export function withStore(fn: () => void): void {
383
+ if (!cachedDb || !cachedDb.db.isOpen) {
384
+ throw new Error(STORE_NOT_OPEN_MESSAGE);
385
+ }
386
+ withTransaction(cachedDb.db, fn);
387
+ }
388
+
389
389
  async function migrateLegacy(db: RawDb): Promise<void> {
390
390
  const legacyPath = legacyHashStorePath();
391
391
  let content: string;
@@ -431,18 +431,11 @@ async function migrateLegacy(db: RawDb): Promise<void> {
431
431
  ]);
432
432
  }
433
433
  if (rows.length > 0) {
434
- withBusyRetry(() => {
435
- db.exec("BEGIN IMMEDIATE");
436
- try {
437
- const stmt = db.prepare(
438
- "INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, line_checksums, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
439
- );
440
- for (const row of rows) stmt.run(...row);
441
- db.exec("COMMIT");
442
- } catch (e) {
443
- try { db.exec("ROLLBACK"); } catch {}
444
- throw e;
445
- }
434
+ withTransaction(db, () => {
435
+ const stmt = db.prepare(
436
+ "INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, line_checksums, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
437
+ );
438
+ for (const row of rows) stmt.run(...row);
446
439
  });
447
440
  }
448
441
 
@@ -487,7 +480,6 @@ export function upsertSnapshot(
487
480
  ): void {
488
481
  store.stmts.upsert(path, checksum, lineCount, JSON.stringify(hashes), lineChecksums ? JSON.stringify(lineChecksums) : "", Date.now());
489
482
  cacheSnapshot(path, checksum, lineCount, hashes);
490
- touchSession(path);
491
483
  }
492
484
  export function persistSnapshot(
493
485
  store: HashStore,
@@ -540,7 +532,6 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
540
532
  typeof entry.mode === "number" ? entry.mode : null,
541
533
  Date.now(),
542
534
  );
543
- touchSession(path);
544
535
  }
545
536
 
546
537
  export function getUndoEntry(store: HashStore, path: string): UndoRecord | undefined {
@@ -600,7 +591,6 @@ export async function pruneMissing(store: HashStore): Promise<string[]> {
600
591
  }
601
592
  });
602
593
  for (const path of missing) snapshotCache.delete(path);
603
- for (const path of missing) forgetSession(path);
604
594
  return missing;
605
595
  }
606
596
 
@@ -184,9 +184,9 @@ export function planEdit(
184
184
  const fileHashes = precomputedHashes;
185
185
  const warnings: string[] = [];
186
186
 
187
- const rangeFixed = swapReversedRanges(edit, fileHashes, warnings);
187
+ const rangeFixed = swapReversedRanges(edit, fileHashes);
188
188
  const prefixFixed = stripDiffPrefixes(
189
- stripBarePrefixes(rangeFixed, fileHashes, warnings),
189
+ stripBarePrefixes(rangeFixed, warnings),
190
190
  warnings,
191
191
  );
192
192
 
@@ -2,7 +2,7 @@ import { splitLines, truncateToBytes, getCached } from "../utils";
2
2
  import { MAX_HASH_SOURCE_BYTES } from "../constants";
3
3
  import { loadHashStore, type HashStore } from "../hash-store";
4
4
  import { allocateFileAnchors } from "../anchor-registry";
5
- import { xxh32, initHasher } from "./hasher";
5
+ import { xxh32, initHasher, contentChecksum } from "./hasher";
6
6
  import { HASH_LEN, ANCHOR_COUNT, anchorAt, HASH_CLASS, HASH_RUN } from "./alphabet";
7
7
  export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
8
8
 
@@ -15,11 +15,6 @@ export const MAX_HASH_LINES = HASH_SPACE;
15
15
 
16
16
  export const HASH_PROBE_STRIDE = 836286;
17
17
 
18
-
19
- function hashAt(idx: number): string {
20
- return anchorAt(idx);
21
- }
22
-
23
18
  export const HL_PREFIX_PLUS_RE = new RegExp(
24
19
  `^\\+${HASH_RUN}│`,
25
20
  );
@@ -61,6 +56,10 @@ export function hashSource(line: string): string {
61
56
  return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
62
57
  }
63
58
 
59
+ export function lineChecksum(line: string): string {
60
+ return contentChecksum(hashSource(line));
61
+ }
62
+
64
63
  const BITSET_WORDS = Math.ceil(HASH_SPACE / 32);
65
64
 
66
65
  function getBit(bits: Uint32Array, idx: number): boolean {
@@ -88,12 +87,12 @@ function assignHash(used: Uint32Array, baseIdx: number, hint: { value: number })
88
87
  if (!getBit(used, baseIdx)) {
89
88
  setBit(used, baseIdx);
90
89
  hint.value = baseIdx + HASH_PROBE_STRIDE;
91
- return hashAt(baseIdx);
90
+ return anchorAt(baseIdx);
92
91
  }
93
92
  const nextIdx = nextZeroBit(used, hint.value);
94
93
  setBit(used, nextIdx);
95
94
  hint.value = nextIdx + HASH_PROBE_STRIDE;
96
- return hashAt(nextIdx);
95
+ return anchorAt(nextIdx);
97
96
  }
98
97
 
99
98
  export function _lineHashesPure(content: string): string[] {
@@ -12,6 +12,7 @@ export {
12
12
  initHasher,
13
13
  canon,
14
14
  hashSource,
15
+ lineChecksum,
15
16
  } from "./hash";
16
17
 
17
18
  export {