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.
collections: - name: docs-pages paths: ["docs/**/*.md"] # the document set, read by every toolmeta: schemas: ["./page.schema.json"] # the metadata tool's section; cite never reads itcite: baseline: .manni-cite-baseline.jsonDocument sets
Section titled “Document sets”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:.
The citations manifest
Section titled “The citations manifest”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:
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/#collectionsmanni.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| Key | Type | Default | Mirrors | Description |
|---|---|---|---|---|
allowEmpty | boolean | false | --allow-empty | Treat an input set that resolves to zero files as success instead of an error (exit 2). |
respectGitignore | boolean | true | --no-gitignore | Skip files .gitignore covers when expanding directories and globs. --no-gitignore overrides it for one run. |
root | string | git root | --root | The 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. |
repos | mapping | none | --repo | Names 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. |
baseline | string | none | --baseline | Path 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. |
checkSources | boolean | true | --no-check-sources | Check 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. |
severity | mapping | the defaults | none | Per-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.
More than one checkout
Section titled “More than one checkout”root is one checkout. When the pages cite code from several repositories,
repos names the others. Each name maps to a directory:
cite: root: ../code # entries with no source.repo resolve here repos: api: ../api # relative to this file's directory sdk: ../sdkAn 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.
Git is used when it is available
Section titled “Git is used when it is available”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.
Unknown keys are errors
Section titled “Unknown keys are errors”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`.Discovery
Section titled “Discovery”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.
What relative paths are relative to
Section titled “What relative paths are relative to”| Path | Relative to |
|---|---|
a collection’s paths: and exclude: | the config file’s directory |
root:, baseline: and each repos: value | the 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.
The encryption key
Section titled “The encryption key”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.
Example
Section titled “Example”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:
collections: - name: site paths: ["src/content/docs/**/*.{md,mdx}"]cite: severity: source-moved: error # this repo wants a moved pin to block, not warn# 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.sarifThe 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:
collections: - name: site paths: ["src/content/docs/**/*.{md,mdx}"]cite: repos: api: ../api sdk: ../sdk# 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.sarifThe 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:
collections: - name: docs-pages paths: ["docs/**/*.md"]cite: baseline: .manni-cite-baseline.jsonSeverity
Section titled “Severity”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.
| Rule | Default | Why |
|---|---|---|
source-moved | warning | update 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-ambiguous | error | Two or more copies of the cited lines; the tool will not guess. |
source-changed | error | The lines the sentence rests on are gone from where they were. |
source-never-true | error | The pin did not match even at the commit it records. |
source-missing | error | The path is not a tracked file under the root, or the ciphertext resolves to nothing. |
claim-moved | notice | The pinned page text is found verbatim elsewhere, so nothing drifted. update rewrites the lines. |
claim-moved-ambiguous | warning | The pinned text sits in more than one place, and the tool will not guess. |
claim-reanchored | notice | The words the pin covered are unchanged since the claim’s baseline, and only the layout moved. update re-pins it. |
claim-changed | warning | It 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-missing | warning | On a record, a pointer claim’s value is gone, so the entry points at nothing. remove clears it. |
marker-orphan | error | A marker names an id no entry has. |
marker-invalid | error | A marker the tool cannot read, including one carrying JSON. |
marker-repeated | warning | Two markers name one id; the first anchors it. |
marker-misplaced | warning | A marker line sits inside a paragraph, splitting it. update moves it whatever this says, because a move is repair and not a report. |
anchor-invalid | error | The anchor cannot work, so neither end can be judged. |
entry-invalid | error | An entry the schema refuses, a duplicate id, or a pin prefix that does not match the source. |
quote-drift | error | The fenced block does not reproduce the cited lines. |
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 allA 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.