Keep citations out of the page
A citation entry is eleven lines of YAML, and three of them are hashes. On a page that carries four, the frontmatter a writer opens is mostly pins. This page moves those entries out, into one sidecar manifest per collection, and leaves the pages as prose with a title.
Nothing about the check changes. The same manni cite check covers the same
pages, add and update write the manifest instead of the page, and a
finding lands where a reviewer can act on it. What changes is where the YAML
lives.
Every transcript below is real output from the built tool.
The example repository
Section titled “The example repository”manni.config.yamldocs-citations.yaml the site collection's entriesguides-citations.yaml the guides collection's entriesdocs/ limits.md, retries.md, moved.md, moved-two.md, own.md, entry-invalid.mdguides/guide.mdlib/ limits.ts, moved.tsTwo collections, site and guides, and one manifest each. The pages under
docs/ carry a title and prose. Every citation they have is in
docs-citations.yaml.
-
Declare the manifest on the collection. There is no
cite:key for this. The mechanism is the metadata tool’s external metadata, and the collection declares a manifest that owns thecitationskey:manni.config.yaml collections:- name: sitepaths: ["docs/**/*.md"]externalMetadata:- file: ./docs-citations.yamlkeys: [citations]fileis relative to the config file.keysis the list of page keys this manifest owns, andcitationsis the one cite reads and writes. A manifest may own other keys beside it, and a collection may declare more than one manifest, as long as only one of them ownscitations. -
Move each page’s entries into it. The manifest is a mapping from page path, relative to the manifest, to that page’s keys. Cut the
citations:block out of the page’s frontmatter and paste it under the page’s path:docs-citations.yaml # The site collection's citations, keyed by page path relative to this file.docs/limits.md:citations:- id: fetch-timeoutclaim:lines: 3integrity: sha256-921b21cccab21a4577f224ec4171aa56a3414bb3a5a4704ab8b6f314c46aa094source:file: lib/limits.tslines: 2integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1fclaim.linesstill counts the page body, not the manifest. The page decides where its body starts, whoever keeps the entry. So moving an entry into a manifest never changes a single number in it.docs/limits.md ---title: Limits---# LimitsThe fetch timeout is 10 seconds. -
Confirm the page came out clean.
meta getreads the page alone under--no-config, and reads the merged view when the config governs the run. The pair is the proof:Terminal window manni meta get docs/limits.md --no-config --fields title,citationsdocs/limits.md: title=Limits (asserted)docs/limits.md: citations=(unset)Terminal window manni meta get docs/limits.md --fields title,citationsdocs/limits.md: title=Limits (asserted)docs/limits.md: citations=[{"id":"fetch-timeout","claim":{"lines":3,…}] (asserted)The page’s own bytes carry no citation metadata. Every tool that reads the config still sees the key.
-
Check exactly as before. A fourth column appears on the row, naming the manifest and the line the entry starts on:
Terminal window manni cite check docs/limits.md✓ docs/limits.md✓ fetch-timeout :6 current lib/limits.ts:2 current docs-citations.yaml:51 file checked, 1 passed, 0 failed, 0 findingsThe claim end reads
:6, a line of the page, and the manifest column readsdocs-citations.yaml:5, a line of the manifest. A frontmatter entry leaves that column empty.Which manifest owns a page is decided by every collection in the config, whatever
--collectionor the positional paths select. So a page checked by path still finds its sidecar.--no-configreads frontmatter only, and then the page looks like it has no citations at all.
Where a finding lands
Section titled “Where a finding lands”A claim finding or a marker finding sits on the page line, where a reviewer reads the sentence. A finding about the entry itself sits on the manifest, on the entry’s own first line:
✗ docs/entry-invalid.md ✗ bad-pin /source/integrity must match pattern "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$" (docs-citations.yaml:42)
1 file checked, 0 passed, 1 failed, 1 findingThe file that failed is still the page, because the page is what stopped being
checkable. The location is the manifest, because that is the file to open.
-f github annotates it there, so the comment appears on the manifest’s diff
in the pull request:
::error file=docs-citations.yaml,line=42,title=manni%3Acite/entry-invalid::bad-pin (lib/limits.ts): /source/integrity must match pattern "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$"SARIF drops a location outside the repository, so a manifest kept outside it is reported on the page instead.
Half-moved pages are caught
Section titled “Half-moved pages are caught”If you declare the manifest and forget to delete a page’s own citations:,
the page has two owners for one key. That is an error under both tools, and
the message names the manifest and the collection:
✗ docs/own.md ✓ on-the-page lib/limits.ts current ✗ page "citations" is owned by manifest docs-citations.yaml (collection site); remove it from the document (line 3)
1 file checked, 0 passed, 1 failed, 1 findingmeta validate reports the same page as external:owned. So a half-finished
migration cannot pass quietly, whichever gate runs first.
Writing the manifest
Section titled “Writing the manifest”add, update and manni key rotate edit the manifest in place. They splice
only that page’s citations value, so comments, key order and every other
byte survive, and they read the file back to confirm it.
add needs no flag. Where the entry goes follows the config:
manni cite add docs/retries.md:6 lib/limits.ts:3 --id retries --dry-run--- docs-citations.yaml+++ docs-citations.yaml@@ -55,1 +55,13 @@ integrity: sha256-aebba92fe4cddf100cc781281d1f24ad7c234b6189413e2130d5fe71ed86e023++docs/retries.md:+ citations:+ - id: retries+ claim:+ lines: 3+ integrity: sha256-d33051d7acffdfa8567c4615e8daf62d8c77672a15e9fcdab8cb5077f867abb6+ source:+ file: lib/limits.ts+ lines: 3+ integrity: sha256-e9f5bdf94a12c610b54573d2b66347592887805e59c69b64803a8c0d30edaea3+ commit-sha: 9265563d8ea84a13fea0001b7e531c119f47d070docs/retries.md: added retries to docs-citations.yaml:59 (claim at line 6, sha256-d33051d7…; source lib/limits.ts:3, sha256-e9f5bdf9…, 9265563)The report says the claim is at line 6, which is the line you typed. On a
frontmatter page the same add would have pushed the sentence ten lines down
and reported line 16. The page was not touched here, so nothing moved.
update writes each manifest once per run, however many entries it
repaired:
manni cite update --dry-run docs/moved.md docs/moved-two.mddocs/moved-two.md: fetch-timeout source lib/moved.ts:2 -> lib/moved.ts:4 (moved)docs/moved.md: fetch-timeout source lib/moved.ts:2 -> lib/moved.ts:4 (moved)--- docs-citations.yaml+++ docs-citations.yaml@@ -22,3 +22,3 @@ file: lib/moved.ts- lines: 2+ lines: 4 integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1f@@ -35,3 +35,3 @@ file: lib/moved.ts- lines: 2+ lines: 4 integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1f2 citations rewritten in 2 files, 0 skippedKeying by a page field instead of by path
Section titled “Keying by a page field instead of by path”A manifest keyed by path is orphaned by a git mv. A join: key says to
match on a page field instead, and the entry then travels with the page:
collections: - name: guides paths: ["guides/**/*.md"] externalMetadata: - file: ./guides-citations.yaml keys: [citations] join: slugguide-one: citations: - id: fetch-timeout claim: lines: 3 integrity: sha256-921b21cccab21a4577f224ec4171aa56a3414bb3a5a4704ab8b6f314c46aa094 source: file: lib/limits.ts lines: 2 integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1fThe page that carries slug: guide-one in its frontmatter owns that entry,
wherever the file moves to:
✓ guides/guide.md ✓ fetch-timeout :7 current lib/limits.ts:2 current guides-citations.yaml:4
1 file checked, 1 passed, 0 failed, 0 findingsTwo pages carrying one join: value is refused, because a write would land on
whichever page the run reached last.
The three refusals
Section titled “The three refusals”Each is an operational error, exit 2, raised before any page is read.
| You wrote | You hear |
|---|---|
A URL in file: on the manifest that owns citations. |
manni.config.yaml: collection site: citations cannot come from a URL manifest, because cite writes them. |
Two collections matching one page, both with a manifest owning citations. |
docs/page.md is in collections site and api, and both keep citations in a manifest. |
- on add, to read the page from stdin. |
A page read from stdin has no path, and its citations live in docs-citations.yaml, which is keyed by path. |
The first two are about writing. A manifest cite writes has to be a file it can open. A page whose entries have two owners has nothing to say which manifest wins. A URL manifest would also put a private source path into public CI output, which the output rule exists to prevent.
The third is about identity. A page read from stdin has no path and no recorded fields, so there is no key to look the entry up under. Name the file instead of piping it.