jeopi-hashline 16.2.13

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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * One full-file version observed at a point in time. The tag the model sees is
3
+ * {@link Snapshot.hash}; recovery replays edits against {@link Snapshot.text}.
4
+ */
5
+ export interface Snapshot {
6
+ /** Canonical path this version belongs to. */
7
+ readonly path: string;
8
+ /** Full normalized (LF, no BOM) file text as observed. */
9
+ readonly text: string;
10
+ /** Content-derived tag for {@link Snapshot.text} (see {@link computeFileHash}). */
11
+ readonly hash: string;
12
+ /** Timestamp (ms since epoch) the version was recorded. */
13
+ recordedAt: number;
14
+ /**
15
+ * 1-indexed file lines a producer (read/search) actually *displayed* under
16
+ * this tag. A partial read (range, or a structural summary that collapsed
17
+ * bodies) leaves this sparse; a whole-file read fills every line. Multiple
18
+ * reads of the same content union into one set. `undefined` means "no
19
+ * provenance recorded" — the patcher then skips the seen-line check and
20
+ * applies as before. Mutated in place as more of the same content is read.
21
+ */
22
+ seenLines?: Set<number>;
23
+ }
24
+ /**
25
+ * Storage seam for full-file version snapshots. The patcher calls {@link head}
26
+ * for the latest version of a path and {@link byHashExact} when it needs the
27
+ * specific historical version a section's stale tag names.
28
+ */
29
+ export declare abstract class SnapshotStore {
30
+ /** Most-recently recorded version for `path`, or `null` if none. */
31
+ abstract head(path: string): Snapshot | null;
32
+ /**
33
+ * Recorded version for `path` whose tag equals `hash`, or `null`. When two
34
+ * distinct texts collide on the 16-bit tag, returns the most-recently
35
+ * recorded one; callers that treat the tag as content identity must use
36
+ * {@link byHashExact} (or verify {@link Snapshot.text} via {@link byContent}).
37
+ */
38
+ abstract byHash(path: string, hash: string): Snapshot | null;
39
+ /**
40
+ * Collision-safe {@link byHash}: the single retained version for `path`
41
+ * whose tag equals `hash`, or `null` when none is retained OR when two or
42
+ * more distinct texts collide on the tag. In the collision case there is
43
+ * no way to know which retained text the model's line anchors were minted
44
+ * against, so consumers that replay anchors (recovery, previews) must
45
+ * refuse rather than pick one.
46
+ */
47
+ abstract byHashExact(path: string, hash: string): Snapshot | null;
48
+ /**
49
+ * Recorded version for `path` whose {@link Snapshot.text} equals `fullText`,
50
+ * or `null`. Disambiguates hash collisions where two distinct file states
51
+ * share the same 4-hex tag: the patcher consults this before taking the
52
+ * no-drift path so a colliding live text is never accepted as the exact
53
+ * snapshot the model's line anchors were minted against.
54
+ */
55
+ abstract byContent(path: string, fullText: string): Snapshot | null;
56
+ /**
57
+ * Every retained version whose tag equals `hash`, across all tracked
58
+ * paths. The patcher uses this to recover the intended file when a section
59
+ * names a path that does not exist on disk but carries a tag the store
60
+ * minted — the model mistyped the path of a file it read this session.
61
+ *
62
+ * The base returns no matches (recovery disabled); stores that can
63
+ * enumerate their contents override it to enable tag-based path recovery.
64
+ */
65
+ findByHash(_hash: string): Snapshot[];
66
+ /**
67
+ * Record the full normalized text of `path` and return its content tag.
68
+ * `seenLines` (optional) are the 1-indexed lines the producer displayed;
69
+ * they merge into {@link Snapshot.seenLines} across reads of identical text.
70
+ */
71
+ abstract record(path: string, fullText: string, seenLines?: Iterable<number>): string;
72
+ /**
73
+ * Merge `lines` into the {@link Snapshot.seenLines} of the version whose tag
74
+ * equals `hash`. No-op when no such version is retained (the content aged
75
+ * out or was overwritten). Lets producers attach displayed lines after the
76
+ * tag was already minted (the body is formatted after the hash is computed).
77
+ */
78
+ abstract recordSeenLines(path: string, hash: string, lines: Iterable<number>): void;
79
+ /** Drop the version history for a single path. */
80
+ abstract invalidate(path: string): void;
81
+ /**
82
+ * Move retained version history (and read provenance) from `from` to `to`.
83
+ * No-op when `from` has no history. Used by file moves so tags minted from
84
+ * reads of the source path stay valid at the destination.
85
+ */
86
+ abstract relocate(from: string, to: string): void;
87
+ /** Drop every version history. */
88
+ abstract clear(): void;
89
+ }
90
+ export interface InMemorySnapshotStoreOptions {
91
+ /** Maximum number of distinct paths tracked at once (default 30). LRU eviction. */
92
+ maxPaths?: number;
93
+ /** Maximum full-file versions retained per path (default 4). Oldest dropped first. */
94
+ maxVersionsPerPath?: number;
95
+ /**
96
+ * Global ceiling on retained snapshot text summed across every path's
97
+ * version history, measured in UTF-16 code units (default 64 MiB).
98
+ * Least-recently-used path histories are evicted to stay under it.
99
+ */
100
+ maxTotalBytes?: number;
101
+ }
102
+ /**
103
+ * In-memory {@link SnapshotStore} backed by `lru-cache`. Per-path history is a
104
+ * short ring of full-file versions (oldest dropped first); per-session path
105
+ * tracking is LRU-bounded so cold paths age out automatically.
106
+ *
107
+ * Recording byte-identical content again refreshes recency and reuses the
108
+ * existing tag (read fusion); recording new content unshifts a fresh version
109
+ * onto the front of the path history. Two distinct texts that collide on the
110
+ * short 4-hex tag are retained as separate versions so callers can still tell
111
+ * them apart via {@link Snapshot.text} — the tag is only a fast index, never
112
+ * the identity.
113
+ */
114
+ export declare class InMemorySnapshotStore extends SnapshotStore {
115
+ #private;
116
+ constructor(options?: InMemorySnapshotStoreOptions);
117
+ head(path: string): Snapshot | null;
118
+ byHash(path: string, hash: string): Snapshot | null;
119
+ byHashExact(path: string, hash: string): Snapshot | null;
120
+ byContent(path: string, fullText: string): Snapshot | null;
121
+ findByHash(hash: string): Snapshot[];
122
+ record(path: string, fullText: string, seenLines?: Iterable<number>): string;
123
+ recordSeenLines(path: string, hash: string, lines: Iterable<number>): void;
124
+ invalidate(path: string): void;
125
+ relocate(from: string, to: string): void;
126
+ clear(): void;
127
+ }
@@ -0,0 +1,2 @@
1
+ import type { StreamOptions } from "./types";
2
+ export declare function streamHashLines(source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, options?: StreamOptions): AsyncGenerator<string>;
@@ -0,0 +1,73 @@
1
+ import type { Anchor, Cursor, ParsedRange } from "./types";
2
+ export declare function splitHashlineLines(text: string): string[];
3
+ export declare function cloneCursor(cursor: Cursor): Cursor;
4
+ /** Parse a bare line-number anchor. Throws on malformed input. */
5
+ export declare function parseLid(raw: string, lineNum: number): Anchor;
6
+ export type BlockTarget = {
7
+ kind: "replace";
8
+ range: ParsedRange;
9
+ } | {
10
+ kind: "block";
11
+ anchor: Anchor;
12
+ } | {
13
+ kind: "delete";
14
+ range: ParsedRange;
15
+ } | {
16
+ kind: "delete_block";
17
+ anchor: Anchor;
18
+ } | {
19
+ kind: "insert_before";
20
+ anchor: Anchor;
21
+ } | {
22
+ kind: "insert_after";
23
+ anchor: Anchor;
24
+ } | {
25
+ kind: "insert_after_block";
26
+ anchor: Anchor;
27
+ } | {
28
+ kind: "rem";
29
+ } | {
30
+ kind: "move";
31
+ dest: string;
32
+ } | {
33
+ kind: "bof";
34
+ } | {
35
+ kind: "eof";
36
+ };
37
+ interface TokenBase {
38
+ lineNum: number;
39
+ }
40
+ export type Token = (TokenBase & {
41
+ kind: "blank";
42
+ }) | (TokenBase & {
43
+ kind: "envelope-begin";
44
+ }) | (TokenBase & {
45
+ kind: "envelope-end";
46
+ }) | (TokenBase & {
47
+ kind: "abort";
48
+ }) | (TokenBase & {
49
+ kind: "header";
50
+ path: string;
51
+ fileHash?: string;
52
+ }) | (TokenBase & {
53
+ kind: "op-block";
54
+ target: BlockTarget;
55
+ }) | (TokenBase & {
56
+ kind: "payload-literal";
57
+ text: string;
58
+ }) | (TokenBase & {
59
+ kind: "raw";
60
+ text: string;
61
+ });
62
+ export declare class Tokenizer {
63
+ #private;
64
+ feed(chunk: string): Token[];
65
+ end(): Token[];
66
+ reset(): void;
67
+ tokenizeAll(text: string): Token[];
68
+ tokenize(line: string, lineNum?: number): Token;
69
+ isOp(line: string): boolean;
70
+ isHeader(line: string): boolean;
71
+ isEnvelopeMarker(line: string): boolean;
72
+ }
73
+ export type { ParsedRange } from "./types";
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Pure data types shared across the hashline parser, applier, and patcher.
3
+ * Nothing in this file references a filesystem, agent runtime, or schema
4
+ * library — keep it that way.
5
+ */
6
+ /** A line-number anchor (1-indexed). */
7
+ export interface Anchor {
8
+ line: number;
9
+ }
10
+ /** Where an `insert` edit should land relative to existing content. */
11
+ export type Cursor = {
12
+ kind: "bof";
13
+ } | {
14
+ kind: "eof";
15
+ } | {
16
+ kind: "before_anchor";
17
+ anchor: Anchor;
18
+ } | {
19
+ kind: "after_anchor";
20
+ anchor: Anchor;
21
+ };
22
+ /**
23
+ * A single low-level edit produced by the parser and consumed by the applier.
24
+ * Multi-line replacements decompose to one `insert` per replacement line plus
25
+ * one `delete` per consumed line. Replacement payloads are tagged so the
26
+ * applier can distinguish literal insertion from new content for a deleted
27
+ * line.
28
+ */
29
+ export type Edit = {
30
+ kind: "insert";
31
+ cursor: Cursor;
32
+ text: string;
33
+ lineNum: number;
34
+ index: number;
35
+ mode?: "replacement";
36
+ /**
37
+ * Present on inserts lowered from `insert_after_block N:`: the
38
+ * resolved block's first line. Lets the applier slide a body that
39
+ * claims a depth inside the block back across the block's trailing
40
+ * closer lines (never above this line).
41
+ */
42
+ blockStart?: number;
43
+ } | {
44
+ kind: "delete";
45
+ anchor: Anchor;
46
+ lineNum: number;
47
+ index: number;
48
+ oldAssertion?: string;
49
+ } | {
50
+ /**
51
+ * Deferred block edit (`replace_block N:` / `delete_block N` /
52
+ * `insert_after_block N:`). The exact line span is unknown at parse
53
+ * time — it is computed by {@link resolveBlockEdits} once file text +
54
+ * path (→ language) are available, then expanded into concrete edits:
55
+ * a non-empty `payloads` without `mode` (from `replace_block`) becomes
56
+ * the same `replacement` inserts + deletes that `replace start.=end:`
57
+ * produces; an empty `payloads` (from `delete_block`) becomes a pure
58
+ * range deletion; `mode: "insert_after"` becomes plain `after_anchor`
59
+ * inserts at the block's last line. `applyEdits` never sees this
60
+ * variant.
61
+ */
62
+ kind: "block";
63
+ anchor: Anchor;
64
+ payloads: string[];
65
+ mode?: "insert_after";
66
+ lineNum: number;
67
+ index: number;
68
+ };
69
+ /** File-level operation parsed from a section body (`REM` / `MV`). */
70
+ export type FileOp = {
71
+ kind: "rem";
72
+ } | {
73
+ kind: "move";
74
+ dest: string;
75
+ };
76
+ /** Result of applying a parsed set of edits to a text body. */
77
+ export interface ApplyResult {
78
+ /** Post-edit text body. */
79
+ text: string;
80
+ /** First line number (1-indexed) that changed, or `undefined` for a no-op apply. */
81
+ firstChangedLine?: number;
82
+ /** Diagnostic warnings collected by the parser, patcher, or recovery. */
83
+ warnings?: string[];
84
+ /**
85
+ * Resolved spans for each `replace_block`/`delete_block` op in this apply,
86
+ * in patch order. Present only when the apply matched the tagged content
87
+ * (the common no-drift path), so the line numbers line up with what the
88
+ * caller read. Absent when there were no block ops.
89
+ */
90
+ blockResolutions?: BlockResolution[];
91
+ }
92
+ /** A parsed `[A.=B]` line range. */
93
+ export interface ParsedRange {
94
+ start: Anchor;
95
+ end: Anchor;
96
+ }
97
+ /** Optional hints for {@link splitPatchInput}. */
98
+ export interface SplitOptions {
99
+ /** Resolves absolute paths inside hashline headers to cwd-relative form. */
100
+ cwd?: string;
101
+ /**
102
+ * Fallback path used when the input lacks a `[PATH]` header but contains
103
+ * recognizable hashline operations. Lets streaming previews work before
104
+ * the model has written the header.
105
+ */
106
+ path?: string;
107
+ }
108
+ /** Streaming-formatter knobs for {@link streamHashLines}. */
109
+ export interface StreamOptions {
110
+ /** First line number to use when formatting (1-indexed, default 1). */
111
+ startLine?: number;
112
+ /** Maximum formatted lines per yielded chunk (default 200). */
113
+ maxChunkLines?: number;
114
+ /** Maximum UTF-8 bytes per yielded chunk (default 64 KiB). */
115
+ maxChunkBytes?: number;
116
+ }
117
+ /** Result of {@link buildCompactDiffPreview}. */
118
+ export interface CompactDiffPreview {
119
+ preview: string;
120
+ addedLines: number;
121
+ removedLines: number;
122
+ }
123
+ /** Optional knobs for {@link buildCompactDiffPreview}. */
124
+ export interface CompactDiffOptions {
125
+ /** Added lines kept on each side of a long added-run elision (default 2). */
126
+ maxAddedRunContext?: number;
127
+ /** Back-compat alias for {@link maxAddedRunContext}. */
128
+ maxUnchangedRun?: number;
129
+ }
130
+ /**
131
+ * Resolved 1-indexed inclusive line span of a `replace_block N:` target.
132
+ */
133
+ export interface BlockSpan {
134
+ /** First line of the block (1-indexed, inclusive). */
135
+ start: number;
136
+ /** Last line of the block (1-indexed, inclusive). */
137
+ end: number;
138
+ }
139
+ /**
140
+ * One `replace_block N:` / `delete_block N` / `insert_after_block N:` anchor
141
+ * resolved to its concrete line span. Surfaced on {@link ApplyResult} so the
142
+ * host can echo "block N → lines start.=end" and let the model catch a wrong
143
+ * opener — e.g. a decorator or doc-comment that sits in a separate node
144
+ * outside the resolved block.
145
+ */
146
+ export interface BlockResolution {
147
+ /** The 1-indexed line the block op was anchored on (the `N`). */
148
+ anchorLine: number;
149
+ /** First line of the resolved span (1-indexed, inclusive). */
150
+ start: number;
151
+ /** Last line of the resolved span (1-indexed, inclusive). */
152
+ end: number;
153
+ /** Which block op produced this resolution. */
154
+ op: "replace" | "delete" | "insert_after";
155
+ }
156
+ /** Request handed to a {@link BlockResolver} to resolve one `replace_block N:` anchor. */
157
+ export interface BlockResolverRequest {
158
+ /** Target file path (used to infer language by extension). */
159
+ path: string;
160
+ /** Full text the block must be resolved against (the snapshot the tag names). */
161
+ text: string;
162
+ /** 1-indexed line the block must begin on. */
163
+ line: number;
164
+ }
165
+ /**
166
+ * Resolves a `replace_block N:` anchor to the line span of the syntactic block
167
+ * that begins on line N. Returns `null` when no block can be resolved
168
+ * (unrecognized language, blank/out-of-range line, no node begins there, or the
169
+ * resolved subtree has a syntax error). Pure seam: the hashline core declares
170
+ * the contract; the host injects a tree-sitter-backed implementation.
171
+ */
172
+ export type BlockResolver = (request: BlockResolverRequest) => BlockSpan | null;
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "type": "module",
3
+ "name": "jeopi-hashline",
4
+ "version": "16.2.13",
5
+ "description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
6
+ "homepage": "https://github.com/akillness/jeopi",
7
+ "author": "Can Boluk",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/akillness/jeopi.git",
12
+ "directory": "packages/hashline"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/akillness/jeopi/issues"
16
+ },
17
+ "keywords": [
18
+ "patch",
19
+ "diff",
20
+ "edit",
21
+ "hashline",
22
+ "agent",
23
+ "llm"
24
+ ],
25
+ "main": "./src/index.ts",
26
+ "types": "./dist/types/index.d.ts",
27
+ "scripts": {
28
+ "check": "biome check . && bun run check:types",
29
+ "check:types": "tsgo -p tsconfig.json --noEmit",
30
+ "lint": "biome lint .",
31
+ "test": "bun test --parallel",
32
+ "fix": "biome check --write --unsafe .",
33
+ "fmt": "biome format --write ."
34
+ },
35
+ "dependencies": {
36
+ "diff": "^9.0.0",
37
+ "lru-cache": "11.5.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.3.14"
41
+ },
42
+ "engines": {
43
+ "bun": ">=1.3.14"
44
+ },
45
+ "files": [
46
+ "src",
47
+ "README.md",
48
+ "CHANGELOG.md",
49
+ "dist/types"
50
+ ],
51
+ "exports": {
52
+ ".": {
53
+ "types": "./dist/types/index.d.ts",
54
+ "import": "./src/index.ts"
55
+ },
56
+ "./grammar.lark": "./src/grammar.lark",
57
+ "./prompt.md": "./src/prompt.md",
58
+ "./*": {
59
+ "types": "./dist/types/*.d.ts",
60
+ "import": "./src/*.ts"
61
+ },
62
+ "./*.js": "./src/*.ts"
63
+ }
64
+ }