pi-hashline-edit-pro 2.6.0 → 2.6.2

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 CHANGED
@@ -101,6 +101,8 @@ Notes:
101
101
  - Every line in the removed range must match what was last shown to you. The extension records the `HASH│content` rows it serves — `read` output, the auto-read block after `write`, the `+HASH│`/` HASH│` rows of post-edit diffs (replace and undo), the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale/ambiguous-anchor feedback — and verifies the whole range against that record before writing. If an interior line changed on disk since it was shown (external editor, formatter-on-save, code generation) or was never shown, the edit is refused with `[E_RANGE_STALE]` and the current range is returned with fresh anchors, so the retry needs no `read`. Edits outside the served record are only possible for files that were never read (for example right after a `write` with auto-read disabled); once the file has been served, every replaced line must have been shown.
102
102
  - After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading.
103
103
  - Do not issue multiple replace calls on the same file in one message; parallel edits split attention across the post-edit diffs and removed lines are easy to miss. Verify each diff before the next edit on that file.
104
+ - Line endings and BOMs survive every edit. The file's line ending is detected from its first newline and restored on write; a file that mixes LF and CRLF (for example a WSL-edited file) is normalized to the first-seen ending.
105
+ - Files with multiple hard links (`nlink > 1`) are rewritten in place rather than via a temp-file rename, so every link keeps seeing the same content; that write is direct rather than atomic.
104
106
 
105
107
  ## Undo
106
108
 
@@ -120,6 +122,14 @@ Enabled by default. After a successful `write` that changes the file, the extens
120
122
  - Auto-read keeps a 50KB display budget. Lines over 50KB are skipped with a marker instead of their content (use `read` for lines up to 200KB).
121
123
  - Toggle at runtime with `/toggle-auto-read`; the setting persists across sessions.
122
124
 
125
+ ## Tool result details
126
+
127
+ All three tools return machine-readable metadata in `details` alongside the model-visible text:
128
+
129
+ - `read`: `details.truncation` (set when the output was truncated), `details.snapshotId` (a `v2|path|ino|mtime|ctime|size` fingerprint of the file), `details.nextOffset` (use as the next `offset`), and `details.metrics` with `truncated` and `next_offset`.
130
+ - `replace`: `details.diff` (the post-edit diff; `+HASH│` and ` HASH│` rows carry the current anchors), `details.patch` (a standard unified patch of the changes, for external tools), `details.firstChangedLine`, `details.snapshotId`, `details.classification` (`"noop"` when nothing changed), and `details.metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`.
131
+ - `undo_last_replace`: `details.diff` (the undo diff with the restored anchors), `details.patch` (a standard unified patch of the restored changes), and `details.metrics` (same shape as `replace`).
132
+
123
133
  ## Settings
124
134
 
125
135
  | Command | Description |
@@ -144,7 +154,7 @@ Unique anchors by construction. If a line's base hash collides with an already-a
144
154
 
145
155
  Hashes live in a persistent per-file store (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) that keeps the hashes of unchanged lines across edits. When a range is replaced, the runtime maps the old content onto the new content and copies hashes for lines that survived; only genuinely new lines get fresh hashes.
146
156
 
147
- The store also keeps a per-file record of the hashes the model was last served (`read` rows, auto-read blocks, post-edit diff rows). `replace` verifies every line of the resolved range against that record before writing; a line whose hash was never served means it either changed on disk after it was shown or was never shown, and the edit is refused with `[E_RANGE_STALE]`. A `write` clears the record, so edits after a write are verified against whatever the next `read` or auto-read block serves.
157
+ The store also keeps a per-file record of the hashes the model was last served (`read` rows, auto-read blocks, post-edit diff rows), pruned to the file's current hashes on every update so removed lines' hashes do not accumulate. `replace` verifies every line of the resolved range against that record before writing; a line whose hash is missing from the record means it either changed on disk after it was shown or was never shown, and the edit is refused with `[E_RANGE_STALE]`. A `write` clears the record, so edits after a write are verified against whatever the next `read` or auto-read block serves.
148
158
 
149
159
  Two guarantees make this safe even with duplicated content:
150
160
 
package/index.ts CHANGED
@@ -88,7 +88,7 @@ export default function (pi: ExtensionAPI): void {
88
88
  DEFAULT_MAX_BYTES,
89
89
  AUTO_READ_MAX,
90
90
  );
91
- await recordServedSafe(absolutePath, preview.servedHashes, "auto-read");
91
+ await recordServedSafe(absolutePath, preview.servedHashes, "auto-read", new Set(fileHashes));
92
92
  return {
93
93
  content: [
94
94
  ...(event.content ?? []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/undo tools for pi-coding-agent. Every line gets a unique 3-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "scripts": {
55
55
  "test": "vitest run",
56
+ "test:unit": "vitest run --config vitest.unit.config.ts",
56
57
  "test:watch": "vitest",
57
58
  "test:coverage": "vitest run --coverage --coverage.thresholds.lines=90 --coverage.thresholds.statements=90 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
58
59
  "lint": "eslint \"src/**/*.ts\" \"index.ts\" \"test/**/*.ts\"",
@@ -5,5 +5,4 @@
5
5
  - `replace`: when copying a line from read output, remove its HASH│ prefix and keep the leading whitespace exactly as shown.
6
6
  - `replace`: replacement_lines is an array of strings, one element per line. Mirror the removed lines exactly, blank lines included: use `[]` to delete the range, `[""]` for a single blank line, `["a", ""]` for a line followed by a blank line, and `["", ""]` for two blank lines. Do not embed `\n` inside an element — each element is exactly one line.
7
7
  - `replace`: when auto-read shows the post-edit diff, its rows are the fresh anchors for the new file — `+HASH│` and ` HASH│` rows carry current hashes and unchanged lines keep their previous hashes, so you can anchor follow-up edits on the diff without re-reading.
8
- - `replace`: `[E_RANGE_STALE]` means a line inside the replaced range changed on disk after it was last shown (or was never shown). Nothing was modified; the error lists the current range as `HASH│content` rows, so retry with those anchors and no `read`.
9
8
  - `replace`: do not issue multiple replace calls on the same file in one message. Issue the next edit only after verifying the previous diff.
package/src/file-kind.ts CHANGED
@@ -119,7 +119,9 @@ export async function loadFileKindAndText(
119
119
  description: detectedMimeType,
120
120
  };
121
121
  }
122
-
122
+ if (detectedMimeType === undefined && sample.includes(0)) {
123
+ return { kind: "binary", description: "contains NUL bytes" };
124
+ }
123
125
 
124
126
  const decoder = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true });
125
127
  let hadUtf8DecodeErrors = false;
package/src/hash-store.ts CHANGED
@@ -1,13 +1,67 @@
1
1
  import { existsSync } from "fs";
2
2
  import { readFile, rename, mkdir, stat } from "fs/promises";
3
- import { DatabaseSync } from "node:sqlite";
4
3
  import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
5
4
  import { errCode, isRec, splitLines } from "./utils";
6
5
  import { initHasher, contentChecksum } from "./hashline/hasher";
7
6
  import { HASH_RE } from "./hashline/alphabet";
8
7
  import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
8
+
9
9
  type SqlParams = (string | number)[];
10
10
 
11
+ interface RawStatement {
12
+ get(...params: SqlParams): unknown;
13
+ all(...params: SqlParams): unknown;
14
+ run(...params: SqlParams): unknown;
15
+ }
16
+ interface RawDb {
17
+ exec(sql: string): void;
18
+ prepare(sql: string): RawStatement;
19
+ close(): void;
20
+ readonly isOpen: boolean;
21
+ }
22
+ export type SqliteEngine = "node:sqlite" | "bun:sqlite";
23
+
24
+ interface BunDbLike {
25
+ exec(sql: string): void;
26
+ prepare(sql: string): {
27
+ get(...params: SqlParams): unknown;
28
+ all(...params: SqlParams): unknown[];
29
+ run(...params: SqlParams): unknown;
30
+ };
31
+ close(): void;
32
+ }
33
+
34
+ let openDbFn: (path: string) => RawDb;
35
+ let sqliteEngine: SqliteEngine;
36
+
37
+ if (typeof process !== "undefined" && (process.versions as Record<string, string | undefined>).bun) {
38
+ const specifier = "bun:sqlite";
39
+ const mod = await import(specifier) as { Database: new (path: string) => BunDbLike };
40
+ sqliteEngine = "bun:sqlite";
41
+ openDbFn = (path) => {
42
+ const db = new mod.Database(path);
43
+ db.exec(`PRAGMA busy_timeout = ${HASH_STORE_BUSY_TIMEOUT}`);
44
+ let closed = false;
45
+ return {
46
+ exec: (sql) => db.exec(sql),
47
+ prepare: (sql) => {
48
+ const stmt = db.prepare(sql);
49
+ return {
50
+ get: (...p) => stmt.get(...p) ?? undefined,
51
+ all: (...p) => stmt.all(...p),
52
+ run: (...p) => stmt.run(...p),
53
+ };
54
+ },
55
+ close: () => { if (!closed) { closed = true; db.close(); } },
56
+ get isOpen() { return !closed; },
57
+ };
58
+ };
59
+ } else {
60
+ const { DatabaseSync } = await import("node:sqlite");
61
+ sqliteEngine = "node:sqlite";
62
+ openDbFn = (path) => new DatabaseSync(path, { timeout: HASH_STORE_BUSY_TIMEOUT }) as unknown as RawDb;
63
+ }
64
+
11
65
  interface Prepared {
12
66
  get: (...params: SqlParams) => Record<string, unknown> | undefined;
13
67
  allPaths: (...params: SqlParams) => Record<string, unknown>[];
@@ -24,7 +78,7 @@ interface Prepared {
24
78
 
25
79
  export interface HashStore {
26
80
  readonly stmts: Prepared;
27
- readonly engine: "node:sqlite";
81
+ readonly engine: SqliteEngine;
28
82
  }
29
83
 
30
84
  export interface UndoRecord {
@@ -116,11 +170,11 @@ function withBusyRetry<T>(fn: () => T): T {
116
170
  throw lastError;
117
171
  }
118
172
 
119
- function openDbWithBusyRetry(storePath: string): { db: DatabaseSync; stmts: Prepared } {
173
+ function openDbWithBusyRetry(storePath: string): { db: RawDb; stmts: Prepared } {
120
174
  return withBusyRetry(() => openDb(storePath));
121
175
  }
122
176
 
123
- let cachedDb: { path: string; db: DatabaseSync; stmts: Prepared } | null = null;
177
+ let cachedDb: { path: string; db: RawDb; stmts: Prepared } | null = null;
124
178
  let opening: { path: string; promise: Promise<HashStore> } | null = null;
125
179
  let exitHandlerRegistered = false;
126
180
  interface SnapshotCacheEntry {
@@ -130,10 +184,8 @@ interface SnapshotCacheEntry {
130
184
  }
131
185
  const snapshotCache = new Map<string, SnapshotCacheEntry>();
132
186
  export const SNAPSHOT_CACHE_LIMIT = 256;
133
- function openDb(storePath: string): { db: DatabaseSync; stmts: Prepared } {
134
- const db = new DatabaseSync(storePath, {
135
- timeout: HASH_STORE_BUSY_TIMEOUT,
136
- });
187
+ function openDb(storePath: string): { db: RawDb; stmts: Prepared } {
188
+ const db = openDbFn(storePath);
137
189
  try {
138
190
  return buildStore(db);
139
191
  } catch (error) {
@@ -145,8 +197,8 @@ function openDb(storePath: string): { db: DatabaseSync; stmts: Prepared } {
145
197
  }
146
198
 
147
199
  function buildStore(
148
- db: DatabaseSync,
149
- ): { db: DatabaseSync; stmts: Prepared } {
200
+ db: RawDb,
201
+ ): { db: RawDb; stmts: Prepared } {
150
202
  db.exec("PRAGMA journal_mode = WAL");
151
203
  db.exec("PRAGMA synchronous = NORMAL");
152
204
  db.exec(
@@ -229,7 +281,7 @@ function buildStore(
229
281
  return { db, stmts };
230
282
  }
231
283
 
232
- function isHealthy(db: DatabaseSync): boolean {
284
+ function isHealthy(db: RawDb): boolean {
233
285
  try {
234
286
  const row = db.prepare("PRAGMA quick_check").get() as { quick_check?: string } | undefined;
235
287
  return row?.quick_check === "ok";
@@ -252,7 +304,7 @@ async function quarantineStore(storePath: string): Promise<void> {
252
304
  }
253
305
  }
254
306
 
255
- function shutdownDb(db: DatabaseSync): void {
307
+ function shutdownDb(db: RawDb): void {
256
308
  try {
257
309
  db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
258
310
  } catch {
@@ -267,7 +319,7 @@ async function openStore(storePath: string): Promise<HashStore> {
267
319
  await mkdir(hashStoreDir(), { recursive: true });
268
320
 
269
321
  let existed = existsSync(storePath);
270
- let opened: { db: DatabaseSync; stmts: Prepared };
322
+ let opened: { db: RawDb; stmts: Prepared };
271
323
  try {
272
324
  opened = openDbWithBusyRetry(storePath);
273
325
  } catch (error) {
@@ -305,13 +357,13 @@ async function openStore(storePath: string): Promise<HashStore> {
305
357
  }
306
358
  }
307
359
 
308
- return { stmts, engine: "node:sqlite" };
360
+ return { stmts, engine: sqliteEngine };
309
361
  }
310
362
 
311
363
  export function loadHashStore(): Promise<HashStore> {
312
364
  const storePath = hashStorePath();
313
365
  if (cachedDb && cachedDb.path === storePath && cachedDb.db.isOpen) {
314
- return Promise.resolve({ stmts: cachedDb.stmts, engine: "node:sqlite" });
366
+ return Promise.resolve({ stmts: cachedDb.stmts, engine: sqliteEngine });
315
367
  }
316
368
  if (opening && opening.path === storePath) {
317
369
  return opening.promise;
@@ -347,7 +399,7 @@ function withStore(fn: () => void): void {
347
399
  });
348
400
  }
349
401
 
350
- async function migrateLegacy(db: DatabaseSync): Promise<void> {
402
+ async function migrateLegacy(db: RawDb): Promise<void> {
351
403
  const legacyPath = legacyHashStorePath();
352
404
  let content: string;
353
405
  try {
@@ -258,7 +258,7 @@ function mapStableHashes(
258
258
 
259
259
  const removedByContent = new Map<string, { hashes: string[]; pos: number }>();
260
260
  for (const entry of removedEntries) {
261
- const key = canon(oldLines[entry.index]!);
261
+ const key = oldLines[entry.index]!;
262
262
  let queue = removedByContent.get(key);
263
263
  if (!queue) {
264
264
  queue = { hashes: [], pos: 0 };
@@ -269,7 +269,7 @@ function mapStableHashes(
269
269
 
270
270
  for (let i = 0; i < newLines.length; i++) {
271
271
  if (newHashes[i]) continue;
272
- const queue = removedByContent.get(canon(newLines[i]!));
272
+ const queue = removedByContent.get(newLines[i]!);
273
273
  if (!queue || queue.pos >= queue.hashes.length) continue;
274
274
  newHashes[i] = queue.hashes[queue.pos]!;
275
275
  queue.pos += 1;
@@ -455,11 +455,6 @@ export function valEdit(
455
455
  }
456
456
  return { resolved: undefined, mismatches, boundaryDups };
457
457
  }
458
- if (startResolved.line > endResolved.line) {
459
- throw new Error(
460
- `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_bounds[0].hash} and ${edit.hash_bounds[1].hash}).`,
461
- );
462
- }
463
458
  const endLine = endResolved.line;
464
459
  const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
465
460
  const canonLines = fileLines.map((line) => canon(line));
package/src/read.ts CHANGED
@@ -216,7 +216,7 @@ export function regRead(pi: ExtensionAPI): void {
216
216
  fileHashes,
217
217
  resolvedPath,
218
218
  );
219
- await recordServedSafe(resolvedPath, preview.servedHashes, "read");
219
+ await recordServedSafe(resolvedPath, preview.servedHashes, "read", new Set(fileHashes));
220
220
  const snapshotId = await safeSnapId(absolutePath, "read");
221
221
  const previewText =
222
222
  hadUtf8DecodeErrors
@@ -139,3 +139,14 @@ export function genDiff(
139
139
 
140
140
  return { diff: output.join("\n"), firstChangedLine };
141
141
  }
142
+
143
+ export function genPatch(
144
+ path: string,
145
+ oldContent: string,
146
+ newContent: string,
147
+ ): string {
148
+ return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
149
+ context: 4,
150
+ headerOptions: Diff.FILE_HEADERS_ONLY,
151
+ });
152
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ReplaceDetails } from "./replace";
2
- import { genDiff } from "./replace-diff";
2
+ import { genDiff, genPatch } from "./replace-diff";
3
3
  import { visLines, clipLine } from "./utils";
4
4
 
5
5
  type TResult = {
@@ -114,6 +114,7 @@ export function buildNoop(input: NoopInput): TResult {
114
114
  content: [{ type: "text", text }],
115
115
  details: {
116
116
  diff: "",
117
+ patch: "",
117
118
  firstChangedLine: undefined,
118
119
  snapshotId,
119
120
  classification: "noop" as const,
@@ -154,6 +155,7 @@ export function buildChanged(input: SuccessInput): TResult {
154
155
  content: [{ type: "text", text }],
155
156
  details: {
156
157
  diff: diffResult.diff,
158
+ patch: genPatch(path, originalNormalized, result),
157
159
  firstChangedLine:
158
160
  editMeta.firstChangedLine ?? diffResult.firstChangedLine,
159
161
  snapshotId,
@@ -7,7 +7,7 @@ import { recordServedDiff } from "./served";
7
7
  import { contentChecksum } from "./hashline/hasher";
8
8
  import { resolveTarget, writeAtomic } from "./fs-write";
9
9
  import { toCwd } from "./paths";
10
- import { toLF, stripBOM, genDiff, restoreEndings, type LineEnding } from "./replace-diff";
10
+ import { toLF, stripBOM, genDiff, genPatch, restoreEndings, type LineEnding } from "./replace-diff";
11
11
  import { cntDiff, splitLines, errCode, makePrepareArguments } from "./utils";
12
12
  import { loadP, loadGuide } from "./prompts";
13
13
  import { buildMetrics } from "./replace-response";
@@ -170,7 +170,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
170
170
  try {
171
171
  const store = await loadHashStore();
172
172
  upsertSnapshot(store, mutationTargetPath, contentChecksum(undo.content), splitLines(undo.content).length, undo.hashes);
173
- recordServedDiff(store, mutationTargetPath, undoDiff);
173
+ recordServedDiff(store, mutationTargetPath, undoDiff, new Set(undo.hashes));
174
174
  } catch (error) {
175
175
  console.error("Failed to restore hash store snapshot after undo:", error);
176
176
  }
@@ -198,6 +198,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
198
198
  ],
199
199
  details: {
200
200
  diff: undoDiff,
201
+ patch: genPatch(path, currentNormalized, undo.content),
201
202
  metrics: buildMetrics({
202
203
  classification: "applied",
203
204
  editsAttempted: 1,
package/src/replace.ts CHANGED
@@ -85,6 +85,7 @@ export type ReqParams = {
85
85
 
86
86
  export type ReplaceDetails = {
87
87
  diff: string;
88
+ patch?: string;
88
89
  firstChangedLine?: number;
89
90
  snapshotId?: string;
90
91
  classification?: "noop";
@@ -255,9 +256,9 @@ export async function execPipeline(
255
256
  } catch (error) {
256
257
  if (options?.noPersist !== true) {
257
258
  if (error instanceof RangeStaleError) {
258
- await recordServedSafe(absolutePath, error.rangeHashes, "range-stale feedback");
259
+ await recordServedSafe(absolutePath, error.rangeHashes, "range-stale feedback", new Set(originalHashes));
259
260
  } else if (error instanceof AnchorMismatchError) {
260
- await recordServedSafe(absolutePath, error.feedbackHashes, "anchor-mismatch feedback");
261
+ await recordServedSafe(absolutePath, error.feedbackHashes, "anchor-mismatch feedback", new Set(originalHashes));
261
262
  }
262
263
  }
263
264
  throw error;
@@ -572,7 +573,7 @@ export function buildToolDef(): ToolDef {
572
573
  };
573
574
  const changed = buildChanged(successInput);
574
575
  if (changed.details.diff) {
575
- await recordServedDiffSafe(mutationTargetPath, changed.details.diff, "post-edit diff");
576
+ await recordServedDiffSafe(mutationTargetPath, changed.details.diff, "post-edit diff", new Set(resultHashes));
576
577
  }
577
578
  return changed;
578
579
  });
package/src/served.ts CHANGED
@@ -20,22 +20,41 @@ export function getServed(store: HashStore, path: string): Set<string> | undefin
20
20
  return new Set(parsed);
21
21
  }
22
22
 
23
- export function recordServed(store: HashStore, path: string, hashes: string[]): void {
24
- if (hashes.length === 0) return;
25
- const existing = getServed(store, path) ?? new Set<string>();
23
+ export function recordServed(
24
+ store: HashStore,
25
+ path: string,
26
+ hashes: string[],
27
+ scope?: ReadonlySet<string>,
28
+ ): void {
29
+ const existing = getServed(store, path);
30
+ if (!existing && hashes.length === 0) return;
31
+ const set = existing ?? new Set<string>();
26
32
  let changed = false;
33
+ if (scope) {
34
+ for (const hash of set) {
35
+ if (!scope.has(hash)) {
36
+ set.delete(hash);
37
+ changed = true;
38
+ }
39
+ }
40
+ }
27
41
  for (const hash of hashes) {
28
- if (!existing.has(hash)) {
29
- existing.add(hash);
42
+ if (!set.has(hash)) {
43
+ set.add(hash);
30
44
  changed = true;
31
45
  }
32
46
  }
33
47
  if (!changed) return;
34
- store.stmts.servedUpsert(path, JSON.stringify([...existing]), Date.now());
48
+ store.stmts.servedUpsert(path, JSON.stringify([...set]), Date.now());
35
49
  }
36
50
 
37
- export function recordServedDiff(store: HashStore, path: string, diff: string): void {
38
- recordServed(store, path, servedHashesFromDiff(diff));
51
+ export function recordServedDiff(
52
+ store: HashStore,
53
+ path: string,
54
+ diff: string,
55
+ scope?: ReadonlySet<string>,
56
+ ): void {
57
+ recordServed(store, path, servedHashesFromDiff(diff), scope);
39
58
  }
40
59
 
41
60
  export function clearServed(store: HashStore, path: string): void {
@@ -46,11 +65,12 @@ export async function recordServedSafe(
46
65
  path: string,
47
66
  hashes: string[],
48
67
  context: string,
68
+ scope?: ReadonlySet<string>,
49
69
  ): Promise<void> {
50
- if (hashes.length === 0) return;
70
+ if (hashes.length === 0 && !scope) return;
51
71
  try {
52
72
  const store = await loadHashStore();
53
- recordServed(store, path, hashes);
73
+ recordServed(store, path, hashes, scope);
54
74
  } catch (error) {
55
75
  console.error(`Failed to record served state (${context}):`, error);
56
76
  }
@@ -60,7 +80,8 @@ export async function recordServedDiffSafe(
60
80
  path: string,
61
81
  diff: string,
62
82
  context: string,
83
+ scope?: ReadonlySet<string>,
63
84
  ): Promise<void> {
64
85
  if (!diff) return;
65
- await recordServedSafe(path, servedHashesFromDiff(diff), context);
86
+ await recordServedSafe(path, servedHashesFromDiff(diff), context, scope);
66
87
  }