Fix a failing check
Your PR’s citation check is red, or it carries a warning you’d like gone. This page decodes the finding and tells you, rule by rule, what happened and which command clears it. You don’t need to know how the citations were set up, only what the output is telling you.
Decode the finding
Section titled “Decode the finding”In the default pretty output, a page with a problem looks like this:
✗ docs/limits.md ✗ fetch-timeout :13 current lib/limits.ts:2 changed since 9265563, 1 commit Check the sentence against the source, then run manni cite update --accept --only fetch-timeout. manni:cite source-changed: The cited lines are not what they were. Guide: https://hawkeyexl.github.io/manni/cite/fix/#source-changedThe first line is the page. When one sidecar keeps every entry the page has, its path follows the page on that line.
Each indented line below it is one citation, and it carries both ends:
<mark> <id> <claim end> <source end> [<manifest>]| Part | What it tells you |
|---|---|
<mark> | ✓ fine, ↕ a warning, ℹ a notice, ✗ an error, · skipped or baselined. |
<id> | The citation’s id. An entry without one leaves the column empty, and the two ends identify it. |
<claim end> | The page lines the sentence occupies, and their status: :13 current, :13 moved -> :15, :13 changed. A marker-anchored claim reads marker :13, and marker :13 -> :11 when the marker is misplaced. A claim that is a metadata value reads as its pointer, such as #/pains/0 current. Empty for a bare pin. |
<source end> | The source as the entry spells it (lib/limits.ts:2, lib/limits.ts:1-3, a bare path for a whole file, or a ciphertext such as ~AQm4…:5), then its status. |
<manifest> | Printed only when the page’s entries come from more than one place, such as frontmatter and a sidecar. It names the sidecar that owns the entry, and the entry’s line in it. Empty for a frontmatter entry. |
Under the row, a finding says what to do in one line. It names the command
that clears it, with --only <id> when the entry has an id. Two dim lines
follow. The first names the rule and says what it means, and the second links
the rule’s section of this page. A finding that is not about an end, such as
marker-orphan, prints its message under the row instead. That message ends
with what to do, and the same two lines follow it. The samples on this page
show the row and the line that says what to do.
In a pull request the same finding is an annotation, titled
manni:cite/source-changed, and its message ends with the same two lines. A
claim or marker finding is annotated on the page line a reviewer reads. A finding about the entry itself is annotated on
the entry’s own line, in the manifest when a sidecar owns it.
Every rule at a glance
Section titled “Every rule at a glance”| Rule | Severity | It means | Do this |
|---|---|---|---|
source-moved | warning | The cited lines are intact, at a different line number. | manni cite update |
source-moved-ambiguous | error | The cited lines now occur in more than one place. | Widen the range, then update --accept --only <id> |
source-changed | error | The cited lines are not what they were. | Read --show-diff, fix the sentence, then update --accept --only <id> |
source-never-true | error | The pin held at no commit in the source’s history. | Check the sentence, then update --accept --only <id> |
source-missing | error | The path names no tracked file, names an undeclared repo, or an encrypted source cannot be decrypted. | Re-add against the new path; or fix --root or the key |
claim-moved | notice | The pinned page text sits at a different line. The pinned metadata value sits at another pointer. | manni cite update |
claim-moved-ambiguous | warning | The pinned page text now occurs in more than one place. The pinned metadata value now occurs at more than one pointer. | Re-add over a range that occurs once, or remove and re-add the pointer |
claim-reanchored | notice | The pinned words are unchanged, and only the layout around them moved. | manni cite update |
claim-changed | warning | The sentence was edited since it was pinned. The metadata value was edited since it was pinned. | Read it, then update --accept --only <id> |
claim-missing | warning | The pinned metadata value is gone from the record. | manni cite remove --only <id> |
marker-orphan | error | A marker names an id no entry has. | Fix the id, or add the entry |
marker-invalid | error | A marker the tool cannot read, including one carrying JSON. | Write an id, and keep the entry out of the body |
marker-repeated | warning | Two markers name one id. | Delete one, or give the second entry its own id |
marker-misplaced | warning | A marker line sits inside a paragraph and splits it. | manni cite update |
anchor-invalid | error | The entry’s anchor cannot work. | Keep one anchor |
entry-invalid | error | An entry is malformed, or an id is repeated. | Fix the entry, or let add write it |
quote-drift | error | The fenced block does not reproduce the cited lines. | Paste the lines again |
The first five are about the source end, the next five about the
claim end, and the rest about the entry and its anchor. update handles
the mechanical ones and refuses the rest, printing each as skipped. It exits
1 when a skipped finding is an error, so you know work is left. Skipped
warnings and notices leave the exit code at 0.
Two words in the output are not rules. current says an end matches its pin,
and skipped says it was not read, which is what --no-check-sources does to
every source end. Neither is a finding, and neither can be named in
severity:.
Source-end rules
Section titled “Source-end rules”source-moved
Section titled “source-moved”⚠ docs/limits.md ↕ fetch-timeout :13 current lib/limits.ts:2 moved -> lib/limits.ts:4 Run manni cite update.The lines the citation pins are still in the file, byte for byte, but they
are at a different line number. Something was added or removed above them.
The page still says something true, so this is a warning and the exit
code stays 0.
Fix. update re-mints the entry over the new range. It rewrites
source.lines and the entry’s commit-sha in place, and changes nothing else:
manni cite update docs/limits.mddocs/limits.md: fetch-timeout source lib/limits.ts:2 -> lib/limits.ts:4 (moved)1 citation rewritten in 1 file, 0 skippedsource-moved-ambiguous
Section titled “source-moved-ambiguous”✗ docs/limits.md ✗ retries :22 current lib/dup.ts:2 moved, 2 candidates (lib/dup.ts:3, lib/dup.ts:5) Widen source.lines until the range occurs once, then run manni cite update --accept --only retries.The pinned lines left their line number and now occur at two or more places
in the file. Picking one would be a guess, so update skips it. This is common
for a one-line pin over a line a file repeats, such as export const RETRIES = 3;.
Fix. Edit the entry’s source.lines to a range wide enough to occur once,
say "2-3", then re-mint the pin over it:
manni cite update --accept --only retries docs/limits.mdsource-changed
Section titled “source-changed”✗ docs/limits.md ✗ fetch-timeout :13 current lib/limits.ts:2 changed since 9265563, 1 commit Check the sentence against the source, then run manni cite update --accept --only fetch-timeout.The bytes at the cited range are not the bytes the pin was minted over, and
they are nowhere else in the file. The sentence on line 13 may no longer be
true. The status names the newest commit in the file’s history where the pin
held, and how many commits have touched the file since. --show-diff prints
their subjects and the diff.
That commit is derived from git, not read from the entry. After a squash
merge it is the squash commit, even though commit-sha names a branch commit
that is gone.
Two variants carry the same meaning with less history. On a shallow clone,
or past 256 commits, it is changed (history unavailable; fetch-depth: 0).
Where git is not available at all, it names the recorded commit-sha, or
none. The run warns once on stderr:
manni: git is not available here, so citations are checked without history: no never-true, no reanchored claims, no diffs, no commit subjects.Run it in CI explains the last two.
Fix. Read the diff, then decide what the sentence should say:
manni cite check --show-diff docs/limits.mdFix the sentence. Then record that the source is now what the page says:
manni cite update --accept --only fetch-timeout docs/limits.mddocs/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 13 re-pinned (changed; now "The fetch timeout is 30 seconds.")2 citations rewritten in 1 file, 0 skippedOne --accept settles both ends, because editing the sentence made the claim
end claim-changed at the same time. If the sentence was still right and only
the code’s shape changed, the same command re-pins the source alone.
source-never-true
Section titled “source-never-true”✗ docs/limits.md ✗ fetch-timeout :13 current lib/limits.ts:4 never true: the pin held at no commit in the history of lib/limits.ts Check the sentence against the source, then run manni cite update --accept --only fetch-timeout.The tool walked back through every commit that touched the file, to its first
one, and searched the whole file at each. No range of the cited length hashed
to the pin. So the pin was never a record of that file. The usual cause is an
entry edited by hand: a source.file and source.integrity that do not
belong together. The recorded commit-sha does not decide it. A move is not a
cause either. After update rewrites a moved range, a later edit to the line
reads source-changed, because the pinned bytes were in the file once.
Fix. Treat it as source-changed: check the sentence against the source,
then re-mint at HEAD with update --accept --only <id>. Both pins are
printed.
source-missing
Section titled “source-missing”✗ docs/limits.md ✗ fetch-timeout :11 current src/gone.ts:2 missing Run manni cite remove --only fetch-timeout, then manni cite add against the file's new path.No tracked file exists at that path under the root, and no file a commit
touched holds the pin. A file that simply moved is source-moved, and one
manni cite update repairs it. Four cases are left here. A file that is gone.
One renamed and edited in the same commit. An untracked destination. And a
checkout too shallow to read the pin’s commit. Where git is available, sources
resolve through git ls-files. So a file that exists but is not committed is
missing too.
Fix. Remove the entry and add it again against the new path:
manni cite remove docs/limits.md --only fetch-timeoutmanni cite add docs/limits.md:11 lib/config/limits.ts:2 --id fetch-timeoutAn encrypted source says why it is missing, and never names the path:
✗ retries :22 current ~AQm4…:5 missing (no encryption key is available to decrypt it) Set MANNI_ENCRYPTION_KEY, or encryptionKey: at the top of manni.config.yaml.The real ciphertext is 82 characters or more, shortened here. The part in parentheses names the cause:
no encryption key is available to decrypt it. The job has no family encryption key. SetMANNI_ENCRYPTION_KEYfrom the secret. A public CI job that has no key and no code runs with--no-check-sourcesinstead; see Public docs, private code.does not decrypt under the current key. The key is not the one the source was encrypted under. After a key rotation, the secret may still hold the old key.no tracked file matches; wrong --root?. The path decrypted, and no tracked file under the root has it. Point--rootat the code checkout, or add the citation again against the new path.no tracked file matches in sdk; wrong cite.repos.sdk?. The entry names thesdkcheckout, and no tracked file there has the decrypted path. Pointcite.repos.sdkat the sdk checkout, or add the citation again against the new path.
An entry whose source.repo names a checkout the run does not know is
missing too. The status names the repo:
✗ retries :28 current sdk:src/client.ts:2 missing (no repo named sdk is declared) Declare sdk under cite.repos in manni.config.yaml, or pass --repo sdk=<dir>.The pages cite a second code checkout that this run has not been told about.
Add the name under cite.repos, with the checkout’s directory relative to the
config file. Or pass --repo sdk=../sdk for one run, which is how CI usually
supplies it. See
more than one checkout.
Claim-end rules
Section titled “Claim-end rules”A claim is body text on any page. A strategy record is a page whose own
metadata declares type: audience, type: persona or type: journey. On a
record, a claim can also be a metadata value, pinned by a JSON Pointer such
as /pains/0. cite reads a pointer claim on records only. Each rule below shows
the body-line case first, then the pointer case where it differs.
Rest each persona on evidence sets pointer
claims up.
claim-moved
Section titled “claim-moved”ℹ docs/limits.md ℹ fetch-timeout :13 moved -> :15 lib/limits.ts:2 current Run manni cite update.The pinned page text is found verbatim at another line. Someone inserted a
paragraph above it, and nothing drifted. This is a notice, the quietest
level there is, and the exit code stays 0.
Fix. update rewrites claim.lines:
manni cite update docs/limits.mddocs/limits.md: fetch-timeout claim line 13 -> 15 (moved)1 citation rewritten in 1 file, 0 skippedA marker-anchored claim reaches this rule only when the marker no longer
anchors the span the pin holds. The marker travels with its text otherwise.
update re-pins it over the span the marker anchors now, with no flag.
A pointer claim moves when its list is reordered, or an item is inserted above it. The pinned value sits at one other pointer:
ℹ records/personas/theo.md records/personas/theo.citations.yaml ℹ terse-messages #/pains/0 moved -> #/pains/1 research:notes/2026-09-12-contributor.md:3 current Run manni cite update.The same update rewrites claim.pointer:
records/personas/theo.md: terse-messages claim #/pains/0 -> #/pains/1 (moved)claim-moved-ambiguous
Section titled “claim-moved-ambiguous”⚠ docs/limits.md ↕ retries :15 moved, 2 candidates (:17, :21) src/limits.ts:3 current Pin a range that occurs once, or anchor the claim with a marker.The pinned text is now at two or more places on the page, and update will
not guess which one the citation supports. A one-line claim over a sentence
the page repeats is the usual cause.
Fix. Pin a range that occurs once, or anchor with a marker, which cannot be ambiguous:
manni cite add docs/limits.md:21 src/limits.ts:3 --id retries --markerA pointer claim is ambiguous when its value now sits at two or more pointers. A goal copied word for word from a pain does this:
⚠ records/personas/theo.md records/personas/theo.citations.yaml ↕ terse-messages #/pains/0 moved-ambiguous (#/goals/0, #/pains/1) research:notes/2026-09-12-contributor.md:3 current Run manni cite remove --only terse-messages, then cite add the pointer you mean.Fix. Remove the entry, then pin the pointer you mean:
manni cite remove --only terse-messages records/personas/theo.mdmanni cite add records/personas/theo.md#/pains/1 research:notes/2026-09-12-contributor.md:3 --id terse-messagesclaim-reanchored
Section titled “claim-reanchored”ℹ docs/limits.md ℹ retries :15 reanchored since 3f9c2a1 -> :15-17 src/limits.ts:3 current Run manni cite update.The words the claim pinned are all still on the page, unchanged since the
newest commit whose page held the pin. Only the layout around them moved. A
rewrapped paragraph does this, and so does a hoisted marker. Nothing drifted,
so this is a notice and the exit code stays 0. The
citations reference
says how the claim’s baseline is found.
Fix. update re-pins the claim over the span it covers now, with no flag:
manni cite update docs/limits.mdclaim-changed
Section titled “claim-changed”⚠ docs/limits.md ↕ fetch-timeout :13 changed lib/limits.ts:2 current Check it still matches the source, then run manni cite update --accept --only fetch-timeout.The pinned page text is gone, so the sentence was edited. This is a warning, not an error, because it fires on any edit to the pinned text, a typo fix included. Under claim lines that is an edit on those lines. Under a marker it is an edit anywhere in the paragraph or block the marker anchors.
Fix. Read the text as it is now, then re-pin it:
manni cite check --show-diff docs/limits.mdmanni cite update --accept --only fetch-timeout docs/limits.mddocs/limits.md: fetch-timeout claim at line 13 re-pinned (changed; now "The fetch timeout is 30 seconds.")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. It skips the
claim when that line is blank, or inside a different kind of block. A sentence
that was reworded and moved has left that line. --accept then re-pins
whatever paragraph now starts there, so read the printed text. If it is not
the sentence, cite add the sentence again.
A pointer claim is changed when the value at its pointer was edited and the pinned value is nowhere else in the record:
⚠ records/personas/theo.md records/personas/theo.citations.yaml ↕ terse-messages #/pains/1 changed since d572997, uncommitted research:notes/2026-09-12-contributor.md:3 current Check it still matches the source, then run manni cite update --accept --only terse-messages.Fix. Read the value against the evidence. --show-diff prints the old
and new value, labelled with the pointer:
--- records/personas/theo.md@d572997:#/pains/1 +++ records/personas/theo.md:#/pains/1 -Error messages are terse with no remediation hint. +Error messages are terse and name no fix.When the evidence still says what the value says, re-pin it. The log prints the value now pinned:
manni cite update --accept --only terse-messages records/personas/theo.mdrecords/personas/theo.md: terse-messages claim at #/pains/1 re-pinned (changed; now "Error messages are terse and name no fix.")1 citation rewritten in 1 file, 0 skippedA blanket --accept judges a value by its words alone, and skips one that
keeps under half of the pinned value’s words. Naming the id with --only
accepts it, and the log says replaced rather than changed.
A team that wants every edit to a cited sentence reviewed sets
claim-changed: error under
severity:.
claim-missing
Section titled “claim-missing”⚠ records/personas/theo.md records/personas/theo.citations.yaml ↕ unknown-schema #/pains/0 missing research:notes/2026-09-12-contributor.md:4 current Run manni cite remove --only unknown-schema. If the claim still stands, cite add it at its pointer now.The value a pointer claim pins is gone from the record. Either the pointer
names no value, or it names the value another entry pins. The second is a
deleted item whose neighbour shifted up into its index. A body-line claim
never reads missing. This is a warning, and the exit code stays 0.
Fix. update skips the entry and names the remedy. Remove the entry:
manni cite remove --only unknown-schema records/personas/theo.mdIf the claim still stands somewhere in the record, pin it at its pointer
now with cite add.
Anchor and entry rules
Section titled “Anchor and entry rules”marker-orphan
Section titled “marker-orphan”✗ docs/limits.md ✗ nope no entry has id "nope". Fix the marker's id, or add the entry with manni cite add. (line 12)A marker, <!-- cite nope -->, names an id and no entry has it. A typo in the
marker, an entry that was deleted, or a claim written ahead of its citation.
Fix. Correct the id in the marker, or add the entry it refers to. Note
that cite true names the id true. To delete the stray marker instead:
manni cite remove docs/limits.md --only nopeTo add the entry, name a line the marker anchors. add adopts the marker, pins
the text it anchors, and leaves the page body as it was:
manni cite add docs/limits.md:13 lib/limits.ts:2 --id nopemarker-invalid
Section titled “marker-invalid”✗ docs/page.md ✗ page invalid marker: a marker names entry ids, not an entry. Move the entry to citations:, in frontmatter or the sidecar, and name its id. (line 12)A word in a marker’s payload is not an id. The commonest case is JSON. Earlier drafts let a marker carry a whole entry, and that is removed. An entry lives in frontmatter or a sidecar manifest, so a source is never written into the body. A page with more than 500 markers reports this rule once as well.
Fix. Move the entry into the frontmatter or the manifest, and leave an id
in the body. cite add --marker --id <id> writes both halves.
marker-repeated
Section titled “marker-repeated”⚠ docs/limits.md ↕ retries retries is named by markers at lines 8 and 20; the first anchors it. Remove retries from the marker at line 20. (line 20)Two markers name one entry. The first one anchors it, and the second does nothing, so this is a warning.
Fix. Delete the second marker, or give the paragraph its own entry and its own id.
marker-misplaced
Section titled “marker-misplaced”⚠ docs/limits.md ↕ host-scope marker :44 -> :41 moved lib/scope.ts:9 current Run manni cite update to move it.The marker sits inside a paragraph, between two lines of text. MDX and
CommonMark both end a paragraph at a comment line, so the page renders two
paragraphs split mid-sentence. The tool reads it the same way, which leaves
the lines above the marker anchored by nothing. Its place is above the first
line of the paragraph, where cite add --marker writes one.
The claim beside it often reads claim-moved, a notice. The pinned text is
still on the page, at a span the marker no longer anchors.
Fix. manni cite update. The marker moves, and the claim is re-pinned
over the whole paragraph. Every entry the move pushed along has its lines
rewritten in the same write. No flag is needed.
A marker whose claim holds nowhere stays put, and says so:
docs/limits.md: robots ↕ skipped: robots: the marker at line 70 stays, because its claim changed since it was pinned. update --accept moves it and re-pins.Read the sentence first with cite check --show-diff, then
update --accept --only <id> moves it and re-pins it.
anchor-invalid
Section titled “anchor-invalid”✗ docs/limits.md ✗ fetch-timeout fetch-timeout has claim lines and a marker. Keep one. (line 15)The entry’s anchor cannot work, so neither end is judged. Five cases reach this rule, and the last two arise on a record only:
| Message | Cause | Fix |
|---|---|---|
fetch-timeout has claim lines and a marker. Keep one. | The entry stores claim.lines and a marker names it too. | Delete the marker, or delete claim.lines. |
fetch-timeout: quote needs a claim or a marker. Add claim.lines over the block, or a marker above it. | quote: true with nothing anchoring the block. | Add claim.lines over the block, or a marker above it. |
fetch-timeout: the quote's claim lines 14-18 are no longer a fenced block. Restore the fences, or re-add the citation over the block. | The lines a quote pins are not fenced any more. | Restore the fences, or re-add the citation over the block. |
terse-messages has a claim pointer and a marker. Keep one. | On a record, the entry stores claim.pointer and a marker names it too. | Delete the marker, or re-add the entry over body lines. |
terse-messages: a quote's claim is a fenced block, so it takes claim.lines, not claim.pointer. | On a record, quote: true beside a pointer claim. | Drop quote, or re-add the entry over the block’s lines. |
Claim lines inside the frontmatter cannot happen. claim.lines counts the
body, so it never points there. add refuses such a line outright:
manni: docs/limits.md:2 is in the frontmatter. A claim is body text.On a record, the refusal names the pointer for the value on that line:
manni: records/personas/theo.md:8 is in the frontmatter. Cite the value there as records/personas/theo.md#/pains/0.entry-invalid
Section titled “entry-invalid”✗ docs/limits.md ✗ fetch-timeout /source must have required property 'integrity' (line 4) ✗ fetch-timeout duplicate id "fetch-timeout". Give each entry its own id. (line 13)An entry does not fit the citation shape. A required field is missing,
source.file does not match the grammar, or two entries share an id. A
range that runs backwards or spans more than 5,000 lines is one too. The line is the entry’s own line, in the manifest when
a sidecar owns it.
Two more cases land here. A pin whose prefix does not match its source is one:
fetch-timeout: an encrypted source is pinned with hmac-sha256-, not sha256-.fetch-timeout: a plain source is pinned with sha256-, not hmac-sha256-.And a page that still carries its own citations: while a manifest owns the
key is the other. Move those entries into the manifest.
On a record, a claim that holds both lines and pointer is one more,
as terse-messages: claim has lines and a pointer. Keep one.. A pointer
entry on a page that is not a record fails the shape outright, as
/claim must NOT have additional properties. cite reads pointers on records
only.
Fix. Correct the entry, or remove it and let add write a well-formed
one. remove reads the entries as they are written. A broken one is still
reachable by its id, or by its pointer when it has none:
manni cite remove docs/limits.md --only /citations/3The citations reference has the full shape and the file grammar.
quote-drift
Section titled “quote-drift”✗ docs/limits.md ✗ limits-block :19-23 current lib/limits.ts:3-5 current ✗ limits-block quote: true, but the fenced block does not reproduce the cited lines. Copy the cited lines into the block again. (line 19)The citation has quote: true, both pins hold, and the fenced block on the
page is not a copy of the cited lines. This often follows an
update --accept on a quoted range. The pin now matches the new source, and
the block on the page still shows the old one.
Fix. Paste the cited lines into the block again.
Reproduce and confirm locally
Section titled “Reproduce and confirm locally”Don’t guess and re-push. Run the exact check on your machine.
-
Run the check on just your page. No install needed:
Terminal window npx @hawkeyexl/manni cite check docs/limits.mdYou’ll see the same line CI showed you. Add
--show-difffor a changed end, on either side. -
Apply the fix from the matching section above.
-
Run it again and confirm the green summary:
✓ docs/limits.md✓ fetch-timeout :13 current lib/limits.ts:4 current1 file checked, 1 passed, 0 failed, 0 findings -
Re-push. Commit the page (and, after
update, its rewritten entry or manifest) and the check goes green.
What the exit code means
Section titled “What the exit code means”| Code | Meaning |
|---|---|
0 | No findings, or only warnings and notices. The check is green. |
1 | At least one error-severity finding that is not in the baseline. This is the red check you came here to fix. For update, that a citation was skipped. |
2 | An operational error. The check could not run. No inputs and no config, an unknown format, update under --no-check-sources, or a URL manifest that owns citations. Or a checkout the pages cite whose directory is missing, as Repo directory not found. The message is on stderr, prefixed manni:. |
A 1 means fix the page or the pin. A 2 means fix the setup, or escalate
to whoever configured the check.
Still stuck?
Section titled “Still stuck?”The CLI reference documents every flag. The citations reference documents the entry shape, the file grammar and the hashing rule. The hashing rule is what decides whether two ranges are “the same bytes”.