Skip to content

Pin a fact to the sentence it is about

Front matter describes a whole page. Some facts belong to one part of it. A page with two procedures, one tested and one not, can only say the page was verified. The untested procedure borrows a date it did not earn.

A meta statement puts the fact beside the block it describes, in the page’s own comment syntax. manni meta validate checks its fields against the page’s schemas. A pin ties it to the exact lines below it, so an edit to those lines is noticed.

A statement is meta, then an optional pin, then one JSON object, all on one line. Put it on the line above the block it describes. Here it declares the Doc Detective test that verifies one procedure:

docs/install.md
---
title: Install manni
description: Install the package and run your first check.
type: how-to
---
## Install
<!-- meta {"verified-by":[{"tool":"doc-detective","check":"install-steps"}]} -->
1. Run `npm install --save-dev @hawkeyexl/manni`.
2. Run `npx manni meta validate docs/`.

Validate the page as usual. The statement has no pin yet, which is a notice, and the run exits 0:

Terminal window
manni meta validate docs/install.md
⚠ docs/install.md
(statement) notice the meta statement at line 9 has no pin; run manni meta derive to pin it to the block below. (line 9, statement) [manni:meta]
manni:meta statement-unpinned: A statement without a pin cannot tell when its block changes.
Guide: https://hawkeyexl.github.io/manni/meta/set-up/inline-statements/
1 file checked, 1 passed, 0 failed, 0 errors, 1 notice

The payload is JSON only, and always one object. Use double quotes, and keep the whole statement on one line.

A statement uses the comment forms a cite marker uses, with meta in place of cite.

FormatForms
Markdown<!-- meta … -->, [comment]: # (meta …), {/* meta … */}
MDX{/* meta … */}, <!-- meta … -->, [comment]: # (meta …)
HTML<!-- meta … -->
XML and DITA (.xml, .dita, .ditamap)<?manni meta …?>, preferred, or <!-- meta … -->
DITA<data name="manni-meta" value='…'/>
AsciiDoc// (meta …)
reStructuredText.. (meta …)

Three rules follow from the forms:

  • Code is not read. A statement inside a fenced block or a backtick span is an example, not a statement.
  • A line form ends at ). [comment]: # (meta …), // (meta …) and .. (meta …) stop at the first ). A payload holding one needs <!-- --> or {/* */}, and validation refuses it otherwise.
  • An XML comment cannot hold --. XML forbids it, so a payload holding -- needs <?manni meta …?>.

In DITA, the <data> element’s value holds the optional pin and the JSON object. Single quotes around it let the JSON keep its double quotes. DITA-OT and content management systems keep <data> where they drop comments and most processing instructions. Each element’s content model decides where <data> may sit, and DITA-OT checks that. manni reads it wherever it appears.

A statement anchors exactly as a cite marker does:

  1. Text on its own line. When text follows the statement on its line, it describes the rest of that line.
  2. The next paragraph. Otherwise it describes the next paragraph. Blank lines and lines holding only markers are skipped, so stacked cite markers and statements all anchor the same block.
  3. A fenced block. When a fenced block comes first, it describes that.

A list is one paragraph, so the statement in the example describes both steps, lines 10 and 11.

A statement in the middle of a paragraph is misplaced. Move it above the paragraph’s first line. A statement with nothing after it anchors nothing, and that is an error.

In XML and DITA, a statement describes an element

Section titled “In XML and DITA, a statement describes an element”

A tag alone on its line is one line of text. Pinning that line would miss an edit inside the element. So in XML and DITA a statement describes a whole element, from its start tag to its end tag:

  • First child. A statement that is an element’s first child describes that element. This is the placement to prefer, because the statement moves with the element when the element is moved or reused.
  • Before an element. Any other statement describes the next element after it.
install.dita
<steps>
<?manni meta {"applies-to":["windows"]}?>
<step><?manni meta {"verified-by":[{"tool":"doc-detective","check":"install"}]}?>
<cmd>Run the installer.</cmd>
</step>
<!-- meta {"concepts":["restart"]} -->
<step>
<cmd>Restart the service.</cmd>
</step>
<step>
<cmd><data name="manni-meta" value='{"concepts":["login"]}'/>Log in.</cmd>
</step>
</steps>

The first statement sits right after <steps>, so it is the first child of <steps> and describes all of it. A statement inside a <step> describes that step instead. The next two statements each describe one whole <step>, so an edit to its <cmd> changes the pin. The <data> element is the first child of the third <cmd>, so it describes that <cmd>. DITA’s content model refuses <data> as a <step>’s first child, so use the instruction there for a whole step. cite markers in XML keep their own line rule.

A pin is the hash of the lines a statement describes. It follows the citations hashing rule. The lines are joined with LF, one byte-order mark is dropped, CRLF becomes LF, and there is no trailing LF. It is written sha256- and 64 lowercase hex digits, after meta.

A statement can sit inside the lines it describes. It may share a line with its text, or be an element’s first child. Other statements can sit there too, as each step’s statement sits inside <steps>. So before hashing, every statement inside the lines is cut out, from its opener to its closer. A <data> carrier is cut whole, from its start tag to its end tag, even across lines. A line that held a statement and is blank after the cut is dropped. A line that was blank before the cut stays.

Writing a pin or a tool’s result into any statement therefore never changes a pin over it. Moving a statement onto or off its own line keeps its pin too.

manni meta derive pins every unpinned statement that describes something:

Terminal window
manni meta derive docs/install.md

It prints pinned 1 statement in 1 page. above its usual summary, and exits 0. The statement now reads:

<!-- meta sha256-384b6729be777c15e9e55ae3f0a0fb1fbe63793abf601e5f517702e9019966e3 {"verified-by":[{"tool":"doc-detective","check":"install-steps"}]} -->

Run it twice and nothing changes. derive never rewrites a pin that already differs from its block. In CI, manni meta derive --check reports each unpinned statement as statement-unpinned and exits 1. Pinning reads no git, because a pin hashes the file’s own lines, never a commit. Outside a git repository, derive still pins statements. Its git-derived fields go unanswered, with one notice line.

Reword step 2, and the pin no longer matches. validate warns, and the run still exits 0:

(statement) warning the block under the meta statement at line 9 changed since it was pinned. Run its checks again, or remove the pin and run manni meta derive. (line 9, statement) [manni:meta]
manni:meta statement-changed: The pinned block no longer matches.
Guide: https://hawkeyexl.github.io/manni/meta/set-up/inline-statements/

When the statement sets last-verified, the message adds that its last-verified may no longer hold. There are two ways forward:

  • Run its checks again. A verifying tool re-pins the statement on its next pass, as Doc Detective does.
  • Accept the edit. Delete the pin token, keep the rest, and run manni meta derive to pin the new text.

A statement on a heading describes the whole section, down to the next heading of the same or a higher level:

<!-- meta {"applies-to":["windows"],"graph":{"type":"task"}} -->
## Install on Windows

The heading is an ATX heading in Markdown and MDX, a title in AsciiDoc, or a section title in reStructuredText. In XML and DITA, it is a statement that is the first child of an element with a <title>, or one placed just before the <title>.

A heading statement differs from a block statement in two ways:

  • It pins the heading only. In XML that means the <title> element’s lines. A section’s facts go stale when its title changes, not on every body edit. Put verification on the blocks it verifies.
  • It may set graph. The value takes the shape of a graph.sections entry, so "graph":{"type":"task"} types the section. The topic types are task, concept, reference, learning, troubleshooting and form. Any other statement that sets graph is an error.

A section has one spelling. When a heading statement and graph.sections in front matter set the same field for one section, that is an error naming both lines. Keep one.

A statement uses the fields the page’s schemas already define. There is no statement vocabulary. Each field is checked alone, against the schema set that resolves for the page, so an override that adds a vocabulary reaches statements too.

  • Defined fields only. A key none of the page’s schemas define is an error. That catches a typo such as verifed-by, and a field the repository has not adopted.
  • Each value against its own definition. last-verified means the same in a statement as in front matter. A value its schema refuses is an ordinary schema finding, marked (line 9, statement).
  • No required fields. A statement describes part of a page, so required never applies.
  • No external fields. A field marked x-manni-location: external lives in the collection’s manifest, so a statement that sets it is refused.

The fields that suit a statement are small facts about one block. last-verified and verified-by come from stewardship, and concepts, applies-to and not-applicable-to from structure.

A statement never merges into the page’s metadata. Page schemas never see it, and meta get --fields reads page fields only.

manni meta get lists a page’s statements under its fields, one line each:

Terminal window
manni meta get title docs/install.md
docs/install.md: title=Install manni (asserted)
docs/install.md: statement at line 9 (lines 10-11, current) {"verified-by":[{"tool":"doc-detective","check":"install-steps"}]}

A heading statement names its section, as (lines 46, section install-on-windows, current). An orphan reads (anchors nothing, unpinned).

In JSON, each page with statements gains a statements list:

"statements": [
{
"line": 9,
"anchor": "10-11",
"scope": "block",
"section": "install",
"pin": "sha256-384b6729be777c15e9e55ae3f0a0fb1fbe63793abf601e5f517702e9019966e3",
"status": "current",
"data": { "verified-by": [{ "tool": "doc-detective", "check": "install-steps" }] }
}
]

scope is block or section, and status is current, changed or unpinned. pin is absent when there is none. data is null when the payload is not one JSON object. The CLI reference has the full shape.

manni graph build reads statements through the same scanner. A heading statement gives its fields to the section node. Any other statement mints a graph:Block node, page.md#install.2 for the second statement under that heading. How graph works has the details.

  • Every field derives as front matter does. Title, description, authors, dates, tags, concepts, typing, provenance and verification all reach the section or block.
  • A section keeps its heading as its title. A heading statement’s title derives nothing.
  • Nothing is copied from the page. A statement’s language is its own lang, never the page’s.
  • Each verified-by entry is a node. Its IRI adds /verified-by/1 to its subject’s, as in page.md#install.2/verified-by/1.
  • The sections derive source carries them. A graph.derive list without sections drops every statement triple.

Each holds a different kind of fact:

UseWhen the fact isExample
A meta statementSmall, and about one block or section of this pageverified-by on one procedure
A cite markerA claim pinned to lines of another fileA sentence pinned to the source it describes
A manifestBookkeeping, or anything largepersonas, evals, owners

A statement carries its data inline. A cite marker carries ids, and its entries live in a manifest. A statement pins only the block it sits on, never another file.

Each statement finding has the rule id manni:meta/<rule>, and its guide is this page. Only an error moves the exit code to 1.

RuleSeverityWhat it means
statement-invaliderrorThe payload is not one JSON object. The message quotes the JSON parser’s reason.
statement-unknown-fielderrorA key none of the page’s schemas define.
statement-external-fielderrorA field marked x-manni-location: external, which lives in the manifest.
statement-orphanerrorNothing follows the statement for it to describe.
statement-line-form-parenerrorA line form whose payload holds ).
statement-xml-comment-dasheserrorAn XML comment whose payload holds --.
statement-graph-not-sectionerrorA statement that is not on a heading sets graph.
statement-conflicts-sectionserrorA heading statement and graph.sections set the same field.
statement-misplacedwarningThe statement sits inside a paragraph.
statement-changedwarningThe pinned block no longer matches.
statement-unpinnednoticeThe statement has no pin.

In JSON, each statement finding carries statement, with its line and its anchor, such as { "line": 9, "anchor": "10-11" }. anchor is null on an orphan.

A verifying tool fills in its own verified-by entry on a pass, sets last-verified and pins the block, in the statement’s own comment form. Write manni metadata from another tool states the rules, and pinOf from @hawkeyexl/manni computes the pin.