@d3ara1n/pi-hashline-edit 0.1.1 → 0.2.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.
@@ -1,84 +0,0 @@
1
- /**
2
- * File snapshot and anchor verification.
3
- *
4
- * A snapshot records the original text + per-line hash at read time; at apply
5
- * time it verifies "current file == snapshot" (stale check) and that each
6
- * anchor's hash matches its line number (guards against the model
7
- * misremembering).
8
- *
9
- * CRLF: splitLines normalizes by stripping the trailing `\r` from each line
10
- * (hashes are based on clean lines, matching the `\r`-free content the model
11
- * copies from the display); createSnapshot records the original line ending,
12
- * and joinLines restores it per the recorded ending — guaranteeing a CRLF file
13
- * keeps its line endings after edit.
14
- *
15
- * @module pi-hashline-edit/core
16
- */
17
-
18
- import { hashFileLines } from "./hash.ts";
19
- import type { Anchor, FileSnapshot, LineEnding } from "./types.ts";
20
-
21
- /**
22
- * Split text into lines, stripping the trailing `\r` of each line (CRLF
23
- * normalization, so hashes are based on clean lines).
24
- *
25
- * Convention: a trailing newline is treated as the terminator of the last line,
26
- * not as producing an extra empty trailing line.
27
- * - `"a\nb\n"` → `["a", "b"]`
28
- * - `"a\r\nb\r\n"` → `["a", "b"]` (`\r` stripped)
29
- * - `"a\n\n"` → `["a", ""]`
30
- * - `""` → `[]`
31
- */
32
- export function splitLines(text: string): string[] {
33
- if (text === "") return [];
34
- const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
35
- return normalized.split("\n").map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l));
36
- }
37
-
38
- /** Detect the dominant line ending of the text (any `\r\n` counts as CRLF). */
39
- export function detectLineEnding(text: string): LineEnding {
40
- return text.includes("\r\n") ? "crlf" : "lf";
41
- }
42
-
43
- /** Join a line array back into text, restoring the given line ending (default LF). Non-empty files end with a newline. */
44
- export function joinLines(lines: readonly string[], ending: LineEnding = "lf"): string {
45
- if (lines.length === 0) return "";
46
- const sep = ending === "crlf" ? "\r\n" : "\n";
47
- return lines.join(sep) + sep;
48
- }
49
-
50
- /** Create a snapshot for a file: record original text + line ending + per-line context-aware hash. */
51
- export function createSnapshot(path: string, text: string, len = 4): FileSnapshot {
52
- const lines = splitLines(text);
53
- const lineHashes = hashFileLines(lines, len);
54
- return { path, lineHashes, text, hashLen: len, lineEnding: detectLineEnding(text) };
55
- }
56
-
57
- /** Anchor verification result. */
58
- export type AnchorVerifyResult =
59
- | { readonly ok: true; readonly line: number }
60
- | {
61
- readonly ok: false;
62
- readonly error: "hash_not_found" | "line_mismatch" | "collision";
63
- /** Line number(s) where the hash actually appears (1-based). */
64
- readonly found?: readonly number[];
65
- };
66
-
67
- /**
68
- * Verify an anchor against the snapshot.
69
- *
70
- * - hash is unique and the line number matches → `ok`
71
- * - hash is unique but the line number differs → `line_mismatch` (`found` gives the real line number; drift, handled by the relocate middleware)
72
- * - hash appears at multiple lines → `collision` (`found` gives all positions)
73
- * - hash does not exist → `hash_not_found` (file changed, needs re-read)
74
- */
75
- export function verifyAnchor(snapshot: FileSnapshot, anchor: Anchor): AnchorVerifyResult {
76
- const found: number[] = [];
77
- for (let i = 0; i < snapshot.lineHashes.length; i++) {
78
- if (snapshot.lineHashes[i] === anchor.hash) found.push(i + 1);
79
- }
80
- if (found.length === 0) return { ok: false, error: "hash_not_found" };
81
- if (found.length > 1) return { ok: false, error: "collision", found };
82
- if (found[0] !== anchor.line) return { ok: false, error: "line_mismatch", found };
83
- return { ok: true, line: anchor.line };
84
- }