pi-hashline-edit-pro 4.2.7 → 4.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "4.2.7",
3
+ "version": "4.2.8",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
@@ -152,13 +152,14 @@ interface SessionState {
152
152
  served: Map<string, Map<string, string>>;
153
153
  everMinted: AnchorMintedSet;
154
154
  probe: number;
155
+ allocatedChecksum: Map<string, string>;
155
156
  }
156
157
 
157
158
  const SIDECAR_SUFFIX = ".registry.jsonl";
158
159
  const SIDECAR_COMPACT_LINES = 5000;
159
160
  const SIDECAR_COMPACT_BYTES = 1024 * 1024;
160
161
  const SIDECAR_COMPACT_CHUNK = 5000;
161
- const SIDECAR_HEADER_BYTES = 64 * 1024;
162
+ export const SIDECAR_HEADER_BYTES = 64 * 1024;
162
163
  const SIDECAR_HEADER_CHUNK = 4096;
163
164
  let currentKey: string | undefined;
164
165
  const sidecarByKey = new Map<string, string>();
@@ -174,7 +175,7 @@ function newSessionState(seed?: string): SessionState {
174
175
  probe = (probe * 256 + byte) % ANCHOR_COUNT;
175
176
  }
176
177
  }
177
- return { owned: new Map(), served: new Map(), everMinted: new Set(), probe };
178
+ return { owned: new Map(), served: new Map(), everMinted: new Set(), probe, allocatedChecksum: new Map() };
178
179
  }
179
180
 
180
181
  function seedServedFromOwned(state: SessionState): void {
@@ -476,6 +477,7 @@ export function clearRegistry(): void {
476
477
  if (!state) return;
477
478
  state.owned.clear();
478
479
  state.served.clear();
480
+ state.allocatedChecksum.clear();
479
481
  appendEvent({ kind: "clear" });
480
482
  }
481
483
 
@@ -532,6 +534,7 @@ export function shadowStateFrom(state: SessionState): SessionState {
532
534
  served: new Map(),
533
535
  everMinted: new ShadowMintedSet(state.everMinted),
534
536
  probe: state.probe,
537
+ allocatedChecksum: state.allocatedChecksum,
535
538
  };
536
539
  }
537
540
 
@@ -743,6 +746,11 @@ export function alignOwnership(
743
746
  return { anchors, freed: freed.map((f) => f.anchor), minted: minted.map((m) => m.anchor) };
744
747
  }
745
748
 
749
+ function snapshotMatchesAllocation(state: SessionState | undefined, path: string, checksum: string): boolean {
750
+ const allocated = state?.allocatedChecksum.get(path);
751
+ return allocated === undefined || allocated === checksum;
752
+ }
753
+
746
754
  export async function allocateFileAnchors(
747
755
  store: HashStore,
748
756
  path: string,
@@ -754,12 +762,14 @@ export async function allocateFileAnchors(
754
762
  },
755
763
  ): Promise<string[]> {
756
764
  ensureRegistry();
765
+ const registry = current();
757
766
  const shadow = options?.shadow === true;
758
767
  const lines = splitLines(content);
759
768
  const checksums = lines.map((line) => contentChecksum(hashSource(line)));
760
769
  if (options?.previous?.spans) {
761
770
  const prevChecksums = splitLines(options.previous.content).map((line) => contentChecksum(hashSource(line)));
762
771
  const aligned = alignOwnershipWithSpans(path, options.previous.hashes, prevChecksums, checksums, options.previous.spans, { shadow });
772
+ if (!shadow && registry) registry.allocatedChecksum.set(path, contentChecksum(content));
763
773
  if (!shadow && options.persist !== false) {
764
774
  persistSnapshot(store, path, content, aligned.anchors, checksums);
765
775
  }
@@ -772,7 +782,7 @@ export async function allocateFileAnchors(
772
782
  prevChecksums = splitLines(options.previous.content).map((line) => contentChecksum(hashSource(line)));
773
783
  } else {
774
784
  const previousState = getAllocatedState(store, path, !shadow);
775
- if (previousState) {
785
+ if (previousState && snapshotMatchesAllocation(registry, path, previousState.contentChecksum)) {
776
786
  prevAnchors = previousState.anchors;
777
787
  prevChecksums = previousState.checksums;
778
788
  if (!prevChecksums && previousState.contentChecksum === contentChecksum(content)) {
@@ -800,6 +810,7 @@ export async function allocateFileAnchors(
800
810
  }
801
811
  return { anchors, freed: [], minted: anchors };
802
812
  })();
813
+ if (!shadow && registry) registry.allocatedChecksum.set(path, contentChecksum(content));
803
814
  if (!shadow && options?.persist !== false) {
804
815
  persistSnapshot(store, path, content, aligned.anchors, checksums);
805
816
  }
@@ -848,7 +859,7 @@ export async function readSidecarHeader(sidecar: string): Promise<string> {
848
859
  const newline = text.indexOf("\n");
849
860
  if (newline >= 0) return text.slice(0, newline);
850
861
  }
851
- return buffer.subarray(0, readBytes).toString("utf-8");
862
+ return readBytes === buffer.length ? "" : buffer.subarray(0, readBytes).toString("utf-8");
852
863
  } finally {
853
864
  await handle.close();
854
865
  }