Skip to content

Require citations in the standard

You own the schema that says what a page must declare. A class of pages in your repository makes claims about code, and you want those claims pinned rather than trusted. This page makes that a rule of the standard. A reference page without a citation then fails the metadata gate, and a citation that stopped holding fails the citation gate.

Two tools do two halves of the work, and neither can do the other’s. manni meta validate decides whether an entry is well formed and whether a page has one at all. manni cite check decides whether the entry still holds. The schema is where the first half is written down.

Every transcript below is real output from the built tool.

The entry shape is already specified, as manni:citations:1.0.0-proposal.4. Do not restate it in your own schema. Two definitions of one key is the drift this composition avoids.

The vocabulary is not one of the built-in schemas, and no published URL serves it yet, because the draft is open for review. So manni meta schemas vendor, which downloads an http(s) URL, has nothing to download. Copy 1.0.0-proposal.4.json into your repository beside your own schemas, and commit it:

schema/citations-1.0.0-proposal.4.json
schema/reference-page.schema.json

Your schema carries the house rule and nothing the vocabulary already says:

schema/reference-page.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/reference-page.schema.json",
"title": "A reference page must carry at least one citation",
"type": "object",
"required": ["title", "citations"],
"properties": {
"title": { "type": "string", "minLength": 1 }
},
"additionalProperties": true
}

citations is required, and nothing else about it is said here. The vocabulary already requires a non-empty list, requires source on every entry, pins the hash formats, and rejects a stray citation-* key at the page root. The two ends lists every field it constrains.

A schema set applies each member whole, and reports the union of what they find, each finding tagged with the schema that produced it. A page passes only when it satisfies every member. That is how the two halves compose:

manni.config.yaml
collections:
- name: reference
paths: ["docs/reference/**/*.md"]
meta:
overrides:
- collection: reference
schemas:
- ./schema/reference-page.schema.json
- ./schema/citations-1.0.0-proposal.4.json

An override entry carries exactly one of collection: and files:. Use files: "docs/reference/**" where the class of pages is not a collection you already declared. Paths in config are relative to the config file. The precedence chain says which level wins, and validating against more than one schema says what a set does.

Three pages, three outcomes, one run:

Terminal window
manni meta validate docs/reference/
✗ docs/reference/entry-invalid.md
/citations/0/source must have required property 'integrity' (line 5) [./schema/citations-1.0.0-proposal.4.json]
/citations warning "citations" is stored in the page; schema/citations-1.0.0-proposal.4.json prefers external metadata. Run manni meta relocate. (line 3) [location:external]
⚠ docs/reference/limits.md
/citations warning "citations" is stored in the page; schema/citations-1.0.0-proposal.4.json prefers external metadata. Run manni meta relocate. (line 3) [location:external]
✗ docs/reference/retries.md
(root) must have required property 'citations' (line 1) [./schema/reference-page.schema.json]
3 files checked, 1 passed, 2 failed, 2 errors, 2 warnings

The page with no citation fails under your schema. The page with a malformed one fails under the vocabulary. Each finding names the file to open and the schema that objected, so a contributor knows which rule they met. The two warnings do not fail the run. They say the vocabulary prefers citations in the collection’s manifest rather than the page, a choice settled below.

The schema says a page must carry a citation. It cannot say what should happen when a citation stops holding, because that is a comparison against the files as they are now. manni cite check decides it, and severity: is where your policy goes:

manni.config.yaml
cite:
severity:
claim-changed: error # every edit to a cited sentence gets reviewed
source-moved: error # a moved pin blocks until update runs
marker-repeated: off # not reported at all

The one worth deciding deliberately is claim-changed. It fires on any edit to a pinned paragraph, a typo fix beside the cited sentence included. At its default it is a warning, reported and green:

Terminal window
manni cite check docs/reference/limits.md
⚠ docs/reference/limits.md
↕ fetch-timeout :15 changed lib/limits.ts:2 current
1 file checked, 1 passed, 0 failed, 1 finding (1 warning)

With claim-changed: error the same run blocks the branch, and the person who reworded the sentence has to confirm it still rests on the same line:

✗ docs/reference/limits.md
✗ fetch-timeout :15 changed lib/limits.ts:2 current
1 file checked, 0 passed, 1 failed, 1 finding

Choose error where a cited sentence is a contract, as in a reference page a support team quotes. Choose the default where the same repository carries prose that gets copy-edited weekly. A red build on a typo fix teaches contributors to stop citing. A team that wants the ratchet without the noise sets error and turns on a baseline, so only new findings fail.

Level Reported Fails the run
error yes yes, exit 1, unless a baseline records it
warning yes no
notice yes no
off no no

All fourteen rules, their defaults and the reasoning behind each default are in the configuration reference. A level the tool does not know, or a rule name it does not have, is an operational error naming the supported set.

This is a policy decision, made once per collection, and it is yours rather than each author’s. The key, the schema and every rule are the same either way.

What differs Frontmatter Sidecar manifest
Where the YAML sits In each page, above the prose In one file per collection
What an author opens A page whose header is mostly hashes A page with a title
A git mv Moves the entries with the page Orphans the entry, unless the manifest uses join:
Merge conflicts On the page, between two people editing prose On one file, between everyone adding citations
Review The pin sits beside the sentence it pins The pin is a second file in the diff
A public page citing private code Publishes an entry, even when the path is encrypted Publishes nothing on the page

Pick the sidecar when the pages are written by people who should not have to read a hash, and when the sources are private. Pick frontmatter when a page carries one or two citations and reviewers want the pin in the same diff as the sentence.

Declare it on the collection, and say so in the standard, so the answer is not re-litigated per page:

manni.config.yaml
collections:
- name: reference
paths: ["docs/reference/**/*.md"]
externalMetadata:
- file: ./reference-citations.yaml
keys: [citations]

Three rules live in the tool rather than in JSON Schema, because a pattern cannot express them. Write them into the prose of your standard, so an author meets them before CI does.

  • Ids are unique within a page. The vocabulary says so in its description and cannot enforce it over a list whose members may omit the id. A duplicate is entry-invalid at check time, on the second entry’s line:

    ✗ fetch-timeout lib/limits.ts:2 current
    ✗ duplicate id "fetch-timeout" (line 13)
  • A range must not end before it starts. lines: "9-3" passes the pattern and is refused by add and by check.

  • A pin has to hold. No schema can compare a hash with a file. That is the whole of what manni cite check adds, and it is why the citation gate runs beside the metadata gate rather than inside it.