Get started
This page takes you from a sentence to a citation CI can check. You’ll pin one claim, read what got written, and run the check. Then you’ll make the source change and move, and the sentence too, to see how each is reported and what fixes it. Finally you’ll anchor a citation with a marker, and pin a fenced code block.
Every transcript below is real output from the built tool.
Before you start
Section titled “Before you start”- Node.js 24 or newer. Check with
node --version. - A git repository holding both the page and the source it cites. manni
cite finds sources through
git ls-filesand reads history throughgit show. So an untracked file is reportedmissing, and a shallow clone loses thesource-never-truestatus and the diff. Where git is not available, manni cite walks the directory instead and warns once. The check still classifies both ends, just without commits.
manni cite ships in the same package as manni meta. If you have one, you have the other.
npx @hawkeyexl/manni cite --helpnpm install -g @hawkeyexl/mannimanni cite --helpPin a claim
Section titled “Pin a claim”The example repo has one source file and one page. The page says something the source decides:
export const MAX_FILES = 10_000;export const FETCH_TIMEOUT_MS = 10_000;export const RETRIES = 3;---title: Limits---# Limits
The fetch timeout is 10 seconds. It isnot configurable.
Retries default to 3.The sentence sits on lines 6 and 7, as an editor numbers them. That is what
you give add, after the page, and it is the whole claim: nothing is copied
into the entry.
-
Preview the citation.
addtakes the page with its lines, then the source with its lines.--dry-runprints the diff and writes nothing:Terminal window manni cite add docs/limits.md:6-7 lib/limits.ts:2 --id fetch-timeout --dry-run--- docs/limits.md+++ docs/limits.md@@ -2,2 +2,12 @@title: Limits+citations:+ - id: fetch-timeout+ claim:+ lines: 3-4+ integrity: sha256-93f59d1e26513d9a8be099c55aa8ba06c90020f079d7fb16d4a7ca65851f0ec3+ source:+ file: lib/limits.ts+ lines: 2+ integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1f+ commit-sha: 9265563d8ea84a13fea0001b7e531c119f47d070--- -
Write it. Drop
--dry-run:Terminal window manni cite add docs/limits.md:6-7 lib/limits.ts:2 --id fetch-timeoutdocs/limits.md: added fetch-timeout to frontmatter (claim at lines 16-17, sha256-93f59d1e…; source lib/limits.ts:2, sha256-78af1d33…, 9265563) -
Read what it wrote. One entry landed in the frontmatter, and the body was not touched. The entry has two blocks.
claimholds the page lines and a hash of them.sourceholds the file, its lines, a hash of those, and the commit they were read at.Two numbers look different, and both are right. You typed
:6-7, the report said lines 16-17, and the entry storedlines: 3-4. The entry counts the page body, which starts after the frontmatter, so editing the frontmatter never moves a claim. The report counts the file, after the entry pushed the sentence down by ten lines. Everything you type and everything you read is a file line.With a family encryption key available,
source.fileholds a ciphertext instead of the path. Public docs, private code explains when you want that.addrefuses a claim it can already see is wrong, rather than minting a citation it knows is broken:manni: docs/limits.md:2 is in the frontmatter. A claim is body text.manni: docs/limits.md:40 is past the end of the page (32 lines). -
Check it.
Terminal window manni cite check docs/limits.md✓ docs/limits.md✓ fetch-timeout :16-17 current lib/limits.ts:2 current1 file checked, 1 passed, 0 failed, 0 findingsCommit the page. From here on,
manni cite checkis something CI runs on every push; see Run it in CI.
Read a result
Section titled “Read a result”Each citation gets one line under its page, with both ends on it:
<mark> <id> <claim end> <source end> <manifest>| Part | What it tells you |
|---|---|
<mark> |
✓ current, ↕ a warning, ℹ a notice, ✗ an error, · skipped or baselined. The glyph carries the meaning, not only the colour. |
<id> |
The entry’s id. An entry without one leaves the column empty; the two ends say which entry it is. |
<claim end> |
The page lines and their status, as in :16-17 current or :9 moved -> :11. A marker-anchored claim reads marker :30 current. Empty for a bare pin. |
<source end> |
The source as the entry spelled it, then its status: lib/limits.ts:2 current, ~AQm4…:5 changed since 3f9c2a1, 1 commit. |
<manifest> |
Where the entry is kept, when a sidecar owns it, as in docs-citations.yaml:6. Empty for a frontmatter entry. |
The summary line ends every run. findings counts errors, warnings and
notices together, and the parentheses say how many were each. A run is green
only when nothing is an error. The exit code is 0 when every finding is a
warning or a notice. It is 1 when at least one is an error, and 2 when the
tool couldn’t run at all.
When the source changes
Section titled “When the source changes”Someone raises the timeout. FETCH_TIMEOUT_MS = 10_000 becomes 30_000,
and the sentence on the page is no longer true:
manni cite check✗ docs/limits.md ✗ fetch-timeout :16-17 current lib/limits.ts:2 changed since 9265563, 1 commit
1 file checked, 0 passed, 1 failed, 1 findingsource-changed is an error, exit code 1. The check has done its whole
job. It annotated the sentence, named the commit the pin was minted at, and
said how many commits have touched the file since. What the sentence should
say now is a question for a person. --show-diff puts the answer on screen:
manni cite check --show-diff✗ docs/limits.md ✗ fetch-timeout :16-17 current lib/limits.ts:2 changed since 9265563, 1 commit raise fetch timeout to 30s diff --git a/lib/limits.ts b/lib/limits.ts index a5ba35a..83cc3ba 100644 --- a/lib/limits.ts +++ b/lib/limits.ts @@ -1,5 +1,5 @@ export const MAX_FILES = 10_000; -export const FETCH_TIMEOUT_MS = 10_000; +export const FETCH_TIMEOUT_MS = 30_000; export const RETRIES = 3;
export function limits() {
1 file checked, 0 passed, 1 failed, 1 findingA plain update won’t touch it, and says so, with exit code 1 because work
is left undone:
docs/limits.md: fetch-timeout ✗ skipped: changed since 9265563, 1 commit0 citations rewritten in 0 files, 1 skippedSo fix the page. Change the sentence to say 30 seconds. Then re-pin both ends
at HEAD with --accept:
manni cite update --acceptdocs/limits.md: fetch-timeout source lib/limits.ts:2 re-pinned at 9b1e04c (changed; sha256-78af1d33… -> sha256-7cb6e7f1…)docs/limits.md: fetch-timeout claim at line 16 re-pinned (changed; now "The fetch timeout is 30 seconds. It is not configurable.")2 citations rewritten in 1 file, 0 skippedBoth pins are printed, and the accepted claim’s text with them, so the change
is on the record. The next check is current at both ends.
When the source moves
Section titled “When the source moves”Now someone adds two comment lines at the top of lib/limits.ts. The cited
line is line 4, and its bytes are unchanged. The check finds it:
manni cite check⚠ docs/limits.md ↕ fetch-timeout :16-17 current lib/limits.ts:2 moved -> lib/limits.ts:4
1 file checked, 1 passed, 0 failed, 1 finding (1 warning)source-moved is a warning. The page still says something true, so the
exit code is 0 and CI stays green. Rewrite the range with update;
--dry-run shows the diff first:
manni cite update --dry-run--- docs/limits.md+++ docs/limits.md@@ -8,3 +8,3 @@ source: file: lib/limits.ts- lines: 2+ lines: 4docs/limits.md: fetch-timeout source lib/limits.ts:2 -> lib/limits.ts:4 (moved)1 citation rewritten in 1 file, 0 skippedmanni cite updatedocs/limits.md: fetch-timeout source lib/limits.ts:2 -> lib/limits.ts:4 (moved)1 citation rewritten in 1 file, 0 skippedupdate splices the one lines: scalar and touches nothing else: comments,
quoting and the rest of the frontmatter stay as they were. The next check is
current at lib/limits.ts:4.
When the page changes
Section titled “When the page changes”The claim end moves and changes too, and it is classified the same way.
Insert a paragraph above the sentence. The pinned text is found verbatim two lines further down, so nothing drifted:
ℹ docs/limits.md ℹ fetch-timeout :16-17 moved -> :18-19 lib/limits.ts:4 current
1 file checked, 1 passed, 0 failed, 1 finding (1 notice)claim-moved is a notice, the quietest level there is, and update
rewrites claim.lines exactly as it rewrites the source’s:
docs/limits.md: fetch-timeout claim lines 16-17 -> 18-19 (moved)1 citation rewritten in 1 file, 0 skippedEdit the sentence. Now the pinned text is nowhere on the page:
⚠ docs/limits.md ↕ fetch-timeout :16-17 changed lib/limits.ts:4 current
1 file checked, 1 passed, 0 failed, 1 finding (1 warning)claim-changed is a warning, not an error. It fires on any edit to the
pinned paragraph, a typo fix beside the cited sentence included. A red build
for that would block prose work for nothing. Read the new text with
--show-diff, then accept it:
manni cite update --accept --only fetch-timeoutdocs/limits.md: fetch-timeout claim at line 16 re-pinned (changed; now "The fetch timeout is 30 seconds. It is not configurable.")1 citation rewritten in 1 file, 0 skipped--accept re-pins over the paragraph or fenced block now at the claim’s first
line, and prints it. So the log says exactly what was accepted. A sentence
that was reworded and moved cannot be re-pinned this way: cite add it
again.
A team that wants every edit to a cited sentence reviewed sets
claim-changed: error under severity: in the
config.
Anchor with a marker
Section titled “Anchor with a marker”Claim lines are one anchor. A marker is the other, and it is the easier
one to keep true, because it travels with its text. --marker writes the
marker and pins what it anchors:
manni cite add docs/limits.md:9 lib/limits.ts:3 --id retries --markerdocs/limits.md: added retries to frontmatter; marker at line 19, claim pinned at line 20<!-- cite retries -->Retries default to 3. - id: retries claim: integrity: sha256-3049e93e72873542aac2c1c4778fa655e70656f03c08f202444062f404a3315d source: file: lib/limits.ts lines: 3 integrity: sha256-e9f5bdf94a12c610b54573d2b66347592887805e59c69b64803a8c0d30edaea3 commit-sha: d7a5cf43e7e81698327752ce75b01e4c17c66a41The entry has a claim with an integrity and no lines, because the marker
says where it is. Insert as many paragraphs above it as you like and the claim
never moves. Edit the sentence itself and it is claim-changed, exactly as a
line-anchored claim would be.
The marker carries an id and nothing else. Every format has its own comment syntax, and the citations reference lists them all:
| Format | Form |
|---|---|
| Markdown, MDX | <!-- cite retries --> or {/* cite retries */} |
| HTML, XML | <!-- cite retries --> |
| AsciiDoc | // (cite retries) |
| reStructuredText | .. (cite retries) |
An entry is never written into the body. A marker that carries JSON is
marker-invalid, and hears where the entry belongs.
Pin a code block
Section titled “Pin a code block”To pin a fenced block instead of a sentence, use --quote. The block on the
page has to reproduce the cited lines, and add checks that before it writes:
manni cite add docs/limits.md:11-15 lib/limits.ts:1-3 --quote --id limits-blockdocs/limits.md: added an entry to frontmatter (claim lines 21-25, a block that reproduces lib/limits.ts:1-3)```tsexport const MAX_FILES = 10_000;export const FETCH_TIMEOUT_MS = 30_000;export const RETRIES = 3;```The claim pins the whole block, fences included, and the lines inside the
fences are compared with the source. So a block that stops reproducing its
source is quote-drift, even while the pin still holds. A quote can be
marker-anchored too, and then the block is the next fence after the marker.
All three citations show up in one check:
✓ docs/limits.md ✓ fetch-timeout :18-19 current lib/limits.ts:4 current ✓ retries marker :19 current lib/limits.ts:3 current ✓ limits-block :21-25 current lib/limits.ts:1-3 current
1 file checked, 1 passed, 0 failed, 0 findingsNext steps
Section titled “Next steps”You have pinned one claim, cleared a change and a move on each end, and anchored a citation two more ways. To make this a standing guarantee, run the check on every push. If your docs are public and your code is not, set up the two-repo layout.