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 commitThe first line is the page. 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. 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> |
The sidecar that owns the entry, and the entry’s line in it. Empty when the entry lives in the page’s frontmatter. |
In a pull request the same finding is an annotation, titled
manni:cite/source-changed. 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 does not match even at the commit it records. | Check the sentence, then update --accept --only <id> |
source-missing |
error | No tracked file at that path, or an encrypted source that 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. | manni cite update |
claim-moved-ambiguous |
warning | The pinned page text now occurs in more than one place. | Re-add over a range that occurs once |
claim-changed |
warning | The sentence was edited since it was pinned. | Read it, then update --accept --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 |
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 three 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 and
exiting 1 so you know work is left.
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:4The 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 rewrites source.lines 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 the rangeThe 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 commitThe 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 commit the pin was minted at and how many commits
have touched the file since; --show-diff prints their subjects and the diff.
Three variants carry the same meaning with less history. An entry with no
commit-sha is a bare changed. On a shallow clone it is
changed (history unavailable: commit 9265563 not found; fetch-depth: 0).
Where git is not available at all, it is a bare changed too, and the run
warns once on stderr:
manni: git is not available here, so citations are checked without history: no never-true, 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 does not match at 835098cThe tool went back to the commit the entry records and searched the whole
file there. No range of the cited length hashed to the pin. So the pin was
never a record of that file at that commit. The usual cause is an entry
edited by hand: a source.file, source.integrity or source.commit-sha
that does not belong with the others. A move is not a cause. After update
rewrites a moved range, a later edit to the line reads source-changed,
because the pinned bytes were in the file at that commit.
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 missingNo tracked file exists at that path under the root. A renamed file is
missing, deliberately: a pin names a path, and following renames through
history would be a guess. 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 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)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.
Claim-end rules
Section titled “Claim-end rules”claim-moved
Section titled “claim-moved”ℹ docs/limits.md ℹ fetch-timeout :13 moved -> :15 lib/limits.ts:2 currentThe 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 never reaches this rule. The marker travels with its text, so there is nothing to rewrite.
claim-moved-ambiguous
Section titled “claim-moved-ambiguous”⚠ docs/limits.md ↕ retries :15 moved, 2 candidates (:17, :21) src/limits.ts:3 currentThe 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 --markerclaim-changed
Section titled “claim-changed”⚠ docs/limits.md ↕ fetch-timeout :13 changed lib/limits.ts:2 currentThe 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 paragraph, a typo fix beside the cited sentence included.
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 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:.
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" (line 12)A marker, <!-- cite nope -->, names an id and no entry has it. A typo in the
marker, or an entry that was deleted.
Fix. Correct the id in the marker, or add the entry it refers to. Note
that cite true names the id true.
marker-invalid
Section titled “marker-invalid”✗ docs/page.md ✗ page A marker names an entry by id. Write the entry in frontmatter or the sidecar. (line 12)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. (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.
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. Three cases reach this rule:
| 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. |
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. |
The lines a quote pins are not fenced any more. | Restore the fences, or re-add the citation over the block. |
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.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" (line 13)An entry does not fit the citation shape. A required field is missing,
source.file does not match the grammar, a range runs backwards, or two
entries share an id. 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.
Fix. Correct the entry, or delete it and let add write a well-formed
one. The 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 (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. 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”.