Pure hashline primitives: the ANCHOR vocabulary cat mints, grep echoes and
patch spends.
An anchor is <1-based line>:<3-hex content hash> (Can Bölük's original
hashline shape). The LINE NUMBER locates the line; the CONTENT HASH verifies
it. Two coordinates, so a stale or reused anchor can no longer silently land
an edit on the wrong line: when the content sits far from the stated line the
write is REFUSED (:anchor-misplaced) instead of corrupting the file.
This namespace is pure — no IO, no tool wiring, no extension envelope. Every surface that addresses a line routes here so the scheme is never recomputed:
split-content-lines / char-offset-at-line blob <-> line/char coordinates
line-hash / line-anchor / anchor->line text -> <line>:<hash>
render-hashline-block [[ln text]…] -> gutter text
anchor-token / parse-anchor rendered line -> bare anchor
indices-matching-hash content-only hash lookup
resolve-one-anchor / resolve-anchor-range anchor -> live line, or refusal
resolve-anchor-range-read the READ-tolerant twin
resolve-anchor-edit-span anchor span -> char span
Pure hashline primitives: the ANCHOR vocabulary `cat` mints, `grep` echoes and `patch` spends. An anchor is `<1-based line>:<3-hex content hash>` (Can Bölük's original hashline shape). The LINE NUMBER locates the line; the CONTENT HASH verifies it. Two coordinates, so a stale or reused anchor can no longer silently land an edit on the wrong line: when the content sits far from the stated line the write is REFUSED (`:anchor-misplaced`) instead of corrupting the file. This namespace is pure — no IO, no tool wiring, no extension envelope. Every surface that addresses a line routes here so the scheme is never recomputed: split-content-lines / char-offset-at-line blob <-> line/char coordinates line-hash / line-anchor / anchor->line text -> `<line>:<hash>` render-hashline-block [[ln text]…] -> gutter text anchor-token / parse-anchor rendered line -> bare anchor indices-matching-hash content-only hash lookup resolve-one-anchor / resolve-anchor-range anchor -> live line, or refusal resolve-anchor-range-read the READ-tolerant twin resolve-anchor-edit-span anchor span -> char span
(anchor->line anchor)Parse the 1-based line number out of a <line>:<hash> anchor, rendered or
bare.
Parse the 1-based line number out of a `<line>:<hash>` anchor, rendered or bare.
(anchor-string? x)True when x ADDRESSES a line: a bare <line>:<hash> token, or a whole
rendered <line>:<hash>│ <text> line, which is that same anchor with the
gutter still attached. cat uses it to tell an anchor endpoint from a bare
line number; anything else is a number.
True when `x` ADDRESSES a line: a bare `<line>:<hash>` token, or a whole rendered `<line>:<hash>│ <text>` line, which is that same anchor with the gutter still attached. `cat` uses it to tell an anchor endpoint from a bare line number; anything else is a number.
(anchor-token x)The bare <line>:<hash> token inside whatever the caller actually passed.
Every anchor a model ever sees is RENDERED — <line>:<hash>│ <text>, because
cat, grep and patch's re-anchored window all print through
render-hashline-block — and the contract those tools advertise is that one
of their lines goes straight back in as an anchor. So everything from
hashline-gutter on is DECORATION and is cut here: the line number locates
and the hash verifies, while the text behind the gutter only SHOWED the line.
Without this cut the text became part of the hash, no line could carry it,
and the refusal handed back the very anchor it had just refused.
What is left is unwrapped from stray whitespace and quote chars — the other
common JSON/LLM mistake, where "4439:a80" arrives WITH its literal quotes
and parse-long chokes on the leading one.
The bare `<line>:<hash>` token inside whatever the caller actually passed. Every anchor a model ever sees is RENDERED — `<line>:<hash>│ <text>`, because `cat`, `grep` and `patch`'s re-anchored window all print through `render-hashline-block` — and the contract those tools advertise is that one of their lines goes straight back in as an anchor. So everything from `hashline-gutter` on is DECORATION and is cut here: the line number locates and the hash verifies, while the text behind the gutter only SHOWED the line. Without this cut the text became part of the hash, no line could carry it, and the refusal handed back the very anchor it had just refused. What is left is unwrapped from stray whitespace and quote chars — the other common JSON/LLM mistake, where `"4439:a80"` arrives WITH its literal quotes and `parse-long` chokes on the leading one.
(bare-anchor-string? x)True when x is ONLY an anchor — the token, with no line text behind it.
patch asks this of its REPLACEMENT slot: a bare anchor there is the span
the caller forgot to finish, while a rendered line carrying TEXT is plausibly
the content it means to write and is written (with a note), not refused.
True when `x` is ONLY an anchor — the token, with no line text behind it. `patch` asks this of its REPLACEMENT slot: a bare anchor there is the span the caller forgot to finish, while a rendered line carrying TEXT is plausibly the content it means to write and is written (with a note), not refused.
(char-offset-at-line content line-idx)Char offset in content where 0-based line line-idx starts. Returns
(count content) when line-idx reaches past the last line. Public so the
edit-span planner can map line indices back to char positions for splicing.
Char offset in `content` where 0-based line `line-idx` starts. Returns `(count content)` when `line-idx` reaches past the last line. Public so the edit-span planner can map line indices back to char positions for splicing.
How far (in lines) a content hash may sit from its stated line number before
resolve-one-anchor calls the anchor MISPLACED and refuses. The common path
never needs it: an anchor spent right after the cat/grep that minted it
resolves exactly. This window only forgives small drift when anchors are
reused across edits without re-reading; anything larger — the ~200-line gap
of a genuinely wrong or stale anchor, the corruption this whole scheme exists
to stop — is refused so the caller re-reads. Deliberately tight: a false
refuse costs one re-read, a false accept corrupts the file.
How far (in lines) a content hash may sit from its stated line number before `resolve-one-anchor` calls the anchor MISPLACED and refuses. The common path never needs it: an anchor spent right after the `cat`/`grep` that minted it resolves exactly. This window only forgives small drift when anchors are reused across edits without re-reading; anything larger — the ~200-line gap of a genuinely wrong or stale anchor, the corruption this whole scheme exists to stop — is refused so the caller re-reads. Deliberately tight: a false refuse costs one re-read, a false accept corrupts the file.
Hex chars in a line's content hash. The line number locates; this hash
verifies within hash-line-drift-tolerance. Three hex chars keep an anchor
at 4-7 characters while the line coordinate disambiguates collisions.
Hex chars in a line's content hash. The line number locates; this hash verifies within `hash-line-drift-tolerance`. Three hex chars keep an anchor at 4-7 characters while the line coordinate disambiguates collisions.
Separator between the line number and the content hash inside an anchor
(<line>:<hash>). A single char so the gutter stays narrow.
Separator between the line number and the content hash inside an anchor (`<line>:<hash>`). A single char so the gutter stays narrow.
Separator between the anchor and the line text in every rendered block.
U+2502 plus a space: it never occurs in source, so line.split("│ ", 1) is
exact and can never be confused with the : inside the anchor.
Separator between the anchor and the line text in every rendered block.
U+2502 plus a space: it never occurs in source, so `line.split("│ ", 1)` is
exact and can never be confused with the `:` inside the anchor.(indices-matching-hash lines h)0-based indices of lines whose line-hash equals the bare hash h. Pure
content match — the line-number coordinate is applied separately by
resolve-one-anchor, so this only answers "which lines carry this content".
0-based indices of `lines` whose `line-hash` equals the bare hash `h`. Pure content match — the line-number coordinate is applied separately by `resolve-one-anchor`, so this only answers "which lines carry this content".
(line-anchor ln text)The editable anchor for a line: <line-number>:<content-hash> (e.g.
4439:a80). The line number LOCATES the line, the hash VERIFIES its content.
patch parses this back through resolve-anchor-range and refuses when the
hash no longer agrees (the line changed) or when that content now lives far
from the stated line (a stale / misattributed anchor).
The editable anchor for a line: `<line-number>:<content-hash>` (e.g. `4439:a80`). The line number LOCATES the line, the hash VERIFIES its content. `patch` parses this back through `resolve-anchor-range` and refuses when the hash no longer agrees (the line changed) or when that content now lives far from the stated line (a stale / misattributed anchor).
(line-hash line)Stable hash-width-hex-char content hash of line (trimmed). Folds the
spec'd String/hashCode algorithm over the whitespace-trimmed line, so it is
deterministic across JVM runs; the line coordinate disambiguates identical
hashes, and a blank line hashes to 000.
Hot path: it runs once per rendered line on every cat, every grep hit and
every patch resolve. Formats with Integer/toHexString plus a left-pad
rather than java.util.Formatter (~1.5x slower), and leans on the trimmed
String/hashCode because that is a JIT intrinsic.
Stable `hash-width`-hex-char content hash of `line` (trimmed). Folds the spec'd `String/hashCode` algorithm over the whitespace-trimmed line, so it is deterministic across JVM runs; the line coordinate disambiguates identical hashes, and a blank line hashes to `000`. Hot path: it runs once per rendered line on every `cat`, every `grep` hit and every patch resolve. Formats with `Integer/toHexString` plus a left-pad rather than java.util.Formatter (~1.5x slower), and leans on the trimmed `String/hashCode` because that is a JIT intrinsic.
(parse-anchor anchor)Parse a <line>:<hash> anchor into {:line L :hash H} (L a 1-based long, H
the hex content hash). The line number is REQUIRED: an anchor with no :
separator, or a non-numeric line part, parses to {:malformed true :raw S}
and resolve-one-anchor refuses it (:anchor-malformed). Every anchor must
carry BOTH coordinates so the line LOCATES and the hash VERIFIES.
A WHOLE RENDERED LINE parses exactly like a bare token: anchor-token cuts
the gutter and the text behind it first, so the hash is only ever the hex.
Parse a `<line>:<hash>` anchor into `{:line L :hash H}` (L a 1-based long, H
the hex content hash). The line number is REQUIRED: an anchor with no `:`
separator, or a non-numeric line part, parses to `{:malformed true :raw S}`
and `resolve-one-anchor` refuses it (`:anchor-malformed`). Every anchor must
carry BOTH coordinates so the line LOCATES and the hash VERIFIES.
A WHOLE RENDERED LINE parses exactly like a bare token: `anchor-token` cuts
the gutter and the text behind it first, so the hash is only ever the hex.(render-hashline-block tuples)(render-hashline-block tuples indent)Render [[line-number text]…] tuples as the MODEL's addressable gutter —
<line>:<hash>│ <text>, one line per tuple, indent prefixed to each. This
is the single renderer behind cat, grep's hit rows and patch's
re-anchored window, so all three speak one format and any of their lines can
be split on hashline-gutter and fed straight back to patch.
A CRLF file's lines still carry their \r: split-content-lines splits on
\n alone because the char offsets an edit splices at must count every
character the file really has. RENDERING drops that trailing CR — it is
invisible on screen, line-hash never saw it (the hash is of the TRIMMED
line, so the anchor is unchanged), and a model that copies a rendered line
back as a replacement would otherwise write a SECOND carriage return in.
Render `[[line-number text]…]` tuples as the MODEL's addressable gutter — `<line>:<hash>│ <text>`, one line per tuple, `indent` prefixed to each. This is the single renderer behind `cat`, `grep`'s hit rows and `patch`'s re-anchored window, so all three speak one format and any of their lines can be split on `hashline-gutter` and fed straight back to `patch`. A CRLF file's lines still carry their `\r`: `split-content-lines` splits on `\n` alone because the char offsets an edit splices at must count every character the file really has. RENDERING drops that trailing CR — it is invisible on screen, `line-hash` never saw it (the hash is of the TRIMMED line, so the anchor is unchanged), and a model that copies a rendered line back as a replacement would otherwise write a SECOND carriage return in.
(resolve-anchor-edit-span current from-anchor to-anchor replacement)Resolve an anchored line range to a CHAR SPAN against current, WITHOUT
building new content: {:start S :end E :replacement R :from-line N :to-line N}
or {:error {:reason KW …}}. to-anchor defaults to from-anchor (a single
line). The stated line is tried first and only then small drift, so duplicate
hashes elsewhere never make an exact line:hash anchor ambiguous.
Newline semantics: a replacement need NOT end in \n — the matched region's
terminator is preserved (\r\n stays \r\n on a CRLF file) — and one that
DOES is not doubled: exactly one trailing terminator is dropped, so a block
copied with its own newline REPLACES the span instead of growing a blank line
after it. An EMPTY replacement consumes the trailing newline, so the lines
actually vanish instead of leaving blanks behind.
Resolve an anchored line range to a CHAR SPAN against `current`, WITHOUT
building new content: `{:start S :end E :replacement R :from-line N :to-line N}`
or `{:error {:reason KW …}}`. `to-anchor` defaults to `from-anchor` (a single
line). The stated line is tried first and only then small drift, so duplicate
hashes elsewhere never make an exact `line:hash` anchor ambiguous.
Newline semantics: a replacement need NOT end in `\n` — the matched region's
terminator is preserved (`\r\n` stays `\r\n` on a CRLF file) — and one that
DOES is not doubled: exactly one trailing terminator is dropped, so a block
copied with its own newline REPLACES the span instead of growing a blank line
after it. An EMPTY replacement consumes the trailing newline, so the lines
actually vanish instead of leaving blanks behind.(resolve-anchor-range current from-anchor to-anchor)Resolve from-anchor (and to-anchor, defaulting to from-anchor for a
single line) against LIVE current. Each is a <line>:<hash> anchor: the
line number LOCATES it, the hash VERIFIES the content still matches AND still
sits near the stated line (else :anchor-misplaced — the wrong-line guard).
BOTH coordinates are required; a bare hash with no line number is refused
(:anchor-malformed). Returns {:from-line N :to-line N} (1-based,
INCLUSIVE) or {:error {:reason KW …}}.
The WRITE side of the contract. resolve-anchor-range-read is its tolerant
twin, so a read and a write address lines identically but only the write
refuses.
Resolve `from-anchor` (and `to-anchor`, defaulting to `from-anchor` for a
single line) against LIVE `current`. Each is a `<line>:<hash>` anchor: the
line number LOCATES it, the hash VERIFIES the content still matches AND still
sits near the stated line (else `:anchor-misplaced` — the wrong-line guard).
BOTH coordinates are required; a bare hash with no line number is refused
(`:anchor-malformed`). Returns `{:from-line N :to-line N}` (1-based,
INCLUSIVE) or `{:error {:reason KW …}}`.
The WRITE side of the contract. `resolve-anchor-range-read` is its tolerant
twin, so a read and a write address lines identically but only the write
refuses.(resolve-anchor-range-read current from-anchor to-anchor)READ-tolerant twin of resolve-anchor-range, for cat. A read is
NON-DESTRUCTIVE, so a stale hash must not block the look the way it
(correctly) blocks a write. Each anchor still resolves by CONTENT first —
following small drift exactly like the write path — but when its hash matches
no live line the anchor's LINE NUMBER is the fallback: a read can safely show
whatever now sits there. Returns {:from-line N :to-line N :stale? BOOL}
(1-based, INCLUSIVE; the window never inverts) — or {:error …} ONLY for a
genuinely unlocatable anchor (:anchor-malformed, no line number, or
:anchor-line-out-of-range, a line outside the file).
READ-tolerant twin of `resolve-anchor-range`, for `cat`. A read is
NON-DESTRUCTIVE, so a stale hash must not block the look the way it
(correctly) blocks a write. Each anchor still resolves by CONTENT first —
following small drift exactly like the write path — but when its hash matches
no live line the anchor's LINE NUMBER is the fallback: a read can safely show
whatever now sits there. Returns `{:from-line N :to-line N :stale? BOOL}`
(1-based, INCLUSIVE; the window never inverts) — or `{:error …}` ONLY for a
genuinely unlocatable anchor (`:anchor-malformed`, no line number, or
`:anchor-line-out-of-range`, a line outside the file).(resolve-one-anchor lines which {:keys [line hash malformed raw]})Resolve a single PARSED {:line :hash} anchor to a 0-based index in lines,
or {:error {:reason KW …}}. The LINE locates; the hash VERIFIES — but a
NON-UNIQUE hash never blocks a well-located edit:
hash -> use it.hash sits at EXACTLY one line
within hash-line-drift-tolerance -> follow it.hash is AMBIGUOUS (several lines, at least one near the
stated line): the hash cannot choose, but the caller named an
EXPLICIT line, so use it. Duplicate hashes do NOT make a
line:hash anchor ambiguous.hash matches only line(s) FAR from the stated line: a
strong line-vs-content contradiction -> REFUSE. This is the
WRONG-LINE guard that stops an edit landing off target.hash matches no live line (the content is gone) -> REFUSE,
handing back the anchor that IS at the stated line so the
caller recovers in ONE call instead of a second read.
Plus: a malformed anchor (no <line>: prefix) or a line outside the file is
refused — those genuinely cannot be located.Resolve a single PARSED `{:line :hash}` anchor to a 0-based index in `lines`,
or `{:error {:reason KW …}}`. The LINE locates; the hash VERIFIES — but a
NON-UNIQUE hash never blocks a well-located edit:
1. exact - the stated line still hashes to `hash` -> use it.
2. drifted - the line moved a little and `hash` sits at EXACTLY one line
within `hash-line-drift-tolerance` -> follow it.
3. line wins - `hash` is AMBIGUOUS (several lines, at least one near the
stated line): the hash cannot choose, but the caller named an
EXPLICIT line, so use it. Duplicate hashes do NOT make a
`line:hash` anchor ambiguous.
4. misplaced - `hash` matches only line(s) FAR from the stated line: a
strong line-vs-content contradiction -> REFUSE. This is the
WRONG-LINE guard that stops an edit landing off target.
5. not-found - `hash` matches no live line (the content is gone) -> REFUSE,
handing back the anchor that IS at the stated line so the
caller recovers in ONE call instead of a second read.
Plus: a malformed anchor (no `<line>:` prefix) or a line outside the file is
refused — those genuinely cannot be located.(split-content-lines s)Split a file blob into a vec of lines. A trailing empty element (from the file's final newline) is dropped, so the vector's count IS the file's line count and index 0 is line 1.
Split a file blob into a vec of lines. A trailing empty element (from the file's final newline) is dropped, so the vector's count IS the file's line count and index 0 is line 1.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |