Skip to content

Configuration reference

The citation tool reads two keys of the family’s one config file, manni.config.yaml. The top-level collections: list says which pages exist, and the cite: section holds the tool’s own settings. Together they let CI run a bare manni cite check. The config is optional. Every key has a default. The tool runs without a config when two things hold. You pass paths on the command line, and the sources sit in the same repository as the pages.

manni.config.yaml
collections:
- name: docs-pages
paths: ["docs/**/*.md"] # the document set, read by every tool
meta:
schemas: ["./page.schema.json"] # the metadata tool's section; cite never reads it
cite:
baseline: .manni-cite-baseline.json

The pages a bare manni cite check or manni cite update covers come from the top-level collections: list, declared once for every tool. Each collection names its paths and exclude globs, relative to the config file. The keys, their rules and the --collection flag that narrows a run to one are on the metadata tool’s configuration reference. A family file that declares collections: and no cite: section is still this tool’s config: the documents are declared, and every cite key takes its default. The same holds for a file whose only family key is tools: or encryptionKey:.

A collection may keep its pages’ citations in a sidecar instead of in each page’s frontmatter. There is no cite: key for it. The collection declares an externalMetadata: manifest that owns the citations key, and cite reads and writes that file:

manni.config.yaml
collections:
- name: site
paths: ["docs/**/*.{md,mdx}"]
externalMetadata:
- file: "{page}.citations.yaml" # one manifest beside each page
keys: [citations]

{page} is the page’s path relative to the config file, without its extension. So docs/limits.md keeps its citations in docs/limits.citations.yaml. A page with no manifest yet cites nothing, and add creates the file on its first write. This is the recommended layout, because two branches that edit two pages then touch two files. A manifest per page has the full rule and its refusals.

A file without {page}, such as docs-citations.yaml, keeps every page’s entries in one manifest for the collection instead.

Which manifest owns a page is decided by every collection in the file, whatever --collection or the positional paths select. So a page checked by path still finds its sidecar, and --no-config reads frontmatter only. A URL manifest that owns citations is refused (exit 2), because cite writes them. The sidecar has the manifest shape and every refusal.

paths and exclude are not cite: keys. Each is refused with an error that says where it went (exit 2):

manni.config.yaml: "paths" is no longer a cite key. Document sets are declared once for every tool, under a top-level collections: list. See https://hawkeyexl.github.io/manni/meta/reference/configuration/#collections
manni.config.yaml: "exclude" is no longer a cite key. Document sets are declared once for every tool, under a top-level collections: list. See https://hawkeyexl.github.io/manni/meta/reference/configuration/#collections
KeyTypeDefaultMirrorsDescription
allowEmptybooleanfalse--allow-emptyTreat an input set that resolves to zero files as success instead of an error (exit 2).
respectGitignorebooleantrue--no-gitignoreSkip files .gitignore covers when expanding directories and globs. --no-gitignore overrides it for one run.
rootstringgit root--rootThe directory source.file paths resolve from, relative to the config file. It may point outside the repository, at a second checkout. --root overrides it for one run.
reposmappingnone--repoNames each other checkout a citation may cite by source.repo. Each value is that checkout’s directory, relative to the config file. See more than one checkout.
baselinestringnone--baselinePath to a citation baseline, relative to the config file. Setting it turns on --baseline for every run; --no-baseline suppresses it for one. A bare --write-baseline records into this same file.
checkSourcesbooleantrue--no-check-sourcesCheck citations against their sources. false runs the page-side rules only, including every claim end, and reports every source status as skipped, which is not a finding. update refuses it. --no-check-sources turns it off for one run.
severitymappingthe defaultsnonePer-rule severity: error, warning, notice or off. Only the rules named are changed.

The section must be a YAML mapping; an empty cite: is the same as no keys. The booleans must be booleans, so a quoted "false" is rejected rather than read as on. A type error names the key and the type it wanted, and nothing else.

root is one checkout. When the pages cite code from several repositories, repos names the others. Each name maps to a directory:

manni.config.yaml
cite:
root: ../code # entries with no source.repo resolve here
repos:
api: ../api # relative to this file's directory
sdk: ../sdk

An entry whose source.repo is sdk resolves in ../sdk and nowhere else. An entry with no repo still resolves against the root, so adding repos changes no existing entry. A name is lowercase letters, digits and hyphens, starting with a letter or digit. Each value must be a string, and repos itself must be a mapping (exit 2 otherwise).

--repo <name=dir> names a checkout for one run, relative to the working directory. It overrides cite.repos for that name only, adds a name the config lacks, and works with --no-config. So a laptop whose layout differs from CI’s passes --repo sdk=../../oss/sdk and keeps the shared file.

A run opens only the checkouts its pages cite. A job whose collection cites api alone never needs sdk checked out. A cited checkout whose directory is missing stops the run (exit 2). An entry naming a repo that no config or flag declares is source-missing on its sentence. --no-check-sources opens no checkout at all.

Each checkout keeps its own tracked-file index and its own history. A notice about one checkout’s git, such as a shallow clone, is said once per checkout and starts with its name, as in manni: sdk: …. The root’s notices carry no prefix.

No key turns git on or off. The tool uses it whenever git is on PATH and the root is inside a work tree. Sources are then indexed by git ls-files, so only tracked files can be cited. add records HEAD, and history tells source-changed from source-never-true. Otherwise the sources are indexed by a directory walk. A run that would have used git then 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.
manni: git is not available here, so the citation records no commit.

check, update and manni key rotate print the first, when a citation carries a commit, a pin no longer holds, or --show-diff was given. add and update --accept print the second, when a commit would have been recorded.

Any key outside the table above is an operational error (exit 2) naming the key it did not recognize and listing the ones it accepts. The same rule holds inside severity: a rule the tool does not have, or a level other than error, warning, notice and off, is an error naming the supported set.

A dropped allowEmtpy: would otherwise leave a config that reads as configured and is not. The rule also means a config written for a newer manni is rejected outright when it carries a key this version does not know. So pin the version wherever the gate runs.

One former key is refused by name rather than as a typo. salt keyed cited sources before they were encrypted with the family’s encryption key, a top-level encryptionKey: that every tool reads. A config that still carries it hears what replaced it (exit 2), and the message never quotes the value:

manni.config.yaml: "salt" is no longer a cite key. Values are encrypted with a family key: a top-level encryptionKey:, or MANNI_ENCRYPTION_KEY. Run `manni key set`.

The tool looks in the working directory and each ancestor, stopping at the nearest .git boundary. With no .git above the working directory, it looks in the working directory only. In each directory it tries manni.config.yaml, then manni.config.yml.

The first file found wins; ancestor files are never merged. A family file counts when it carries cite:, collections:, tools:, encryptionKey: or providers:. One with none of these belongs to a sibling tool, and discovery keeps looking up the tree. The tool has no config file of its own from before the family file.

-c <path> names the file outright and must exist. --no-config skips discovery and runs on the defaults alone. When a config governs the run, pretty output says so on stderr; the machine formats stay clean.

PathRelative to
a collection’s paths: and exclude:the config file’s directory
root:, baseline: and each repos: valuethe config file’s directory
positional [paths...]the working directory
--root, --repo and --baseline <path>the working directory

So cd docs && manni cite check reaches the same files, the same root and the same baseline as a run from the repository root. Baseline fingerprints match whichever directory the command ran from.

Encrypted sources and their keyed pins use the family encryption key, which is not a cite: key. It is the top-level encryptionKey: of manni.config.yaml, or MANNI_ENCRYPTION_KEY, which wins over it. With a key available, add encrypts every source it writes, and check decrypts each one to check it. The configuration reference has the key itself, and manni key sets and rotates it. Rotation re-encrypts every source.file and re-keys its pin with it, in frontmatter and in a citations manifest alike.

The two-checkout layout, which is the reason root and checkSources exist. The pages are in the public docs repository, so SARIF locations resolve. The sources are reached through --root from a private CI job that checks out both. The file is public, so it carries no encryptionKey:. The key lives in the CI secret MANNI_ENCRYPTION_KEY instead, and the public job runs without it:

manni.config.yaml (public docs repository)
collections:
- name: site
paths: ["src/content/docs/**/*.{md,mdx}"]
cite:
severity:
source-moved: error # this repo wants a moved pin to block, not warn
Terminal window
# Public CI, no access to the code or the key: page-side rules only.
manni cite check --no-check-sources
# Private CI: docs and code checked out side by side; from the docs checkout.
MANNI_ENCRYPTION_KEY="$ENCRYPTION_KEY" manni cite check --root ../code -f sarif > cite.sarif

The public job passes the flag rather than setting checkSources: false. The private job reads the same file, and needs the sources.

When the pages cite two code repositories, the private job checks out all three side by side. The file names the two code checkouts under repos:

manni.config.yaml (public docs repository, two code checkouts)
collections:
- name: site
paths: ["src/content/docs/**/*.{md,mdx}"]
cite:
repos:
api: ../api
sdk: ../sdk
Terminal window
# Private CI: docs, api and sdk checked out side by side; from the docs checkout.
MANNI_ENCRYPTION_KEY="$ENCRYPTION_KEY" manni cite check -f sarif > cite.sarif

The public job still runs --no-check-sources, which opens neither checkout. A repo’s name is printed in plain text, even beside an encrypted path. Public docs, private code says how to choose one.

A single-repository setup needs none of that:

manni.config.yaml (pages and sources in one repository)
collections:
- name: docs-pages
paths: ["docs/**/*.md"]
cite:
baseline: .manni-cite-baseline.json

Every rule has a default severity, and severity: moves any of them. error counts toward exit 1. warning and notice are reported and never move the exit code. off is not reported at all. The rules themselves are defined on the citations reference.

severity: takes exactly these seventeen names. current and skipped are statuses, per end, and neither is a rule, so neither can be named here.

RuleDefaultWhy
source-movedwarningupdate fixes it mechanically, and a PR job that failed on a two-line insert above a cited line would make the ratchet noisy.
source-moved-ambiguouserrorTwo or more copies of the cited lines; the tool will not guess.
source-changederrorThe lines the sentence rests on are gone from where they were.
source-never-trueerrorThe pin did not match even at the commit it records.
source-missingerrorThe path is not a tracked file under the root, or the ciphertext resolves to nothing.
claim-movednoticeThe pinned page text is found verbatim elsewhere, so nothing drifted. update rewrites the lines.
claim-moved-ambiguouswarningThe pinned text sits in more than one place, and the tool will not guess.
claim-reanchorednoticeThe words the pin covered are unchanged since the claim’s baseline, and only the layout moved. update re-pins it.
claim-changedwarningIt fires on an edit to the words the pin covered, a typo fix included. Set it to error to have every such edit reviewed.
claim-missingwarningOn a record, a pointer claim’s value is gone, so the entry points at nothing. remove clears it.
marker-orphanerrorA marker names an id no entry has.
marker-invaliderrorA marker the tool cannot read, including one carrying JSON.
marker-repeatedwarningTwo markers name one id; the first anchors it.
marker-misplacedwarningA marker line sits inside a paragraph, splitting it. update moves it whatever this says, because a move is repair and not a report.
anchor-invaliderrorThe anchor cannot work, so neither end can be judged.
entry-invaliderrorAn entry the schema refuses, a duplicate id, or a pin prefix that does not match the source.
quote-drifterrorThe fenced block does not reproduce the cited lines.
manni.config.yaml
cite:
severity:
source-moved: error # a moved pin blocks until update runs
claim-changed: error # every edit to a cited sentence gets reviewed
marker-repeated: off # not reported at all

A rule set to off produces no finding in any format and never enters a baseline. A rule set to warning renders as ↕ in pretty, ::warning in github, level: warning in SARIF, and a passing testcase in JUnit, exactly as the metadata tool’s severity does. A rule set to notice is reported one level lower: ::notice in github, level: note in SARIF, and never a JUnit failure.