Skip to content

CLI reference

Fifteen commands under one domain, and one subcommand under shapes. Every flag below is verified against src/graph/cli.ts, and the exit codes are exercised by the inline tests at the foot of this page.

Terminal window
manni graph <command> [command options] [arguments]

There is no default subcommand. manni graph docs/ is a usage error, because a domain owns verbs and build is the verb.

Two verbs read documents: build and fill. They take positional paths, which are files, directories, or globs relative to the working directory. With none given they read every collection the config declares, or the ones --collection names. A collection’s paths resolve from the config file’s directory, wherever the command runs. A document’s graph:path and node IRI count from the working directory for a typed path, and from the config file’s directory for a collection. Nothing matching is exit 2, not an empty successful build, unless --allow-empty is given.

- reads one more document from stdin, alongside any named paths and never instead of them. It needs --as, because stdin has no file name to choose a parser by. Its display path is <stdin>, its node is <baseIri>stdin, and its relative links resolve from the working directory. It has no git history. It belongs to no collection, so no external-metadata manifest supplies its fields.

The other verbs read the built graph rather than the documents: check, query, stats, search, traverse, lookup, retrieve, embed and export all load the Turtle file at graph.out, or the one -g names. Build first, then ask. shapes reads neither. It reads the shapes files check runs.

FlagCommandsMeaning
-c, --config <path>all except initPath to manni.config.yaml. Without it, manni graph looks for that file from the working directory up to the repository root. It reads the file’s graph: key plus the top-level collections: and providers: keys. With no file at all, built-in defaults apply and no collections are declared.
--no-configsame as aboveIgnore any discovered config file and run on built-in defaults. It shares one option with -c, so the one written later wins.
--collection <name>build, fillRead only this configured collection. Repeatable, one name per occurrence, never comma-separated. It cannot be combined with positional paths, and it needs a config file to select from. Each of those is exit 2.
--exclude <glob>same as aboveGlob to exclude. Repeatable, one glob per occurrence.
--as <format>same as aboveParse every input as asciidoc, html, markdown, mdx, rst or xml, whatever its extension. Required with -. Another value is exit 2.
--ext <list>same as aboveExtensions a directory walk or a glob keeps, comma-separated and given once. A file named outright is kept whatever this says, when a parser reads its extension. A named file none reads is skipped with a warning.
--allow-emptysame as aboveTreat zero matched files as success.
-g, --graph <path>the graph readersThe built Turtle file. Defaults to the resolved graph.out.
-f, --format <format>every verb that reportspretty or json. check adds github and sarif, and shapes show adds markdown. An unknown value is refused while the options are parsed. A typo therefore costs a message rather than a graph load: exit 2, with the allowed set named.

Flags override config; they do not bypass it. An unset flag falls through to the resolved graph: value, so a config file and the command line reach the same code.

One separator per list. A positional variadic is space-separated because argv is. --ext, --fields and --predicates are written <list>, comma-separated and given once. --collection, --exclude and --shapes are repeatable and never split on commas, because a path or a name may hold one.

Each command’s table below lists every flag it accepts, the shared ones included. A table is the whole surface of its command.

Global options are accepted before the subcommand.

OptionDescription
-V, --versionPrint the manni version and exit.
-h, --helpPrint help for the program or a subcommand.

The knowledge-graph tool. Every command on this page lives under it. graph accepts its own -V, --version, which reports the manni version, and -h, --help.

Terminal window
manni graph <command> [command options] [arguments]
OptionArgumentDefaultDescription
--no-colorn/acolor on a TTYTurn colored output off. NO_COLOR in the environment does the same, and output that is not a terminal is never colored either way.

Writes a starter graph: section into manni.config.yaml in the working directory, and prints the path it wrote. Takes no arguments and no options.

Terminal window
manni graph init

Reads the documents, derives the graph, and writes canonically sorted Turtle. Two runs over unchanged inputs are byte-identical. Dates come from frontmatter and then from git committer times, never from the clock. Every node gets a deterministic IRI rather than a blank node.

A page’s frontmatter is its metadata as manni meta validate reads it. A field kept in a collection’s external-metadata manifest derives exactly as one written on the page.

Terminal window
manni graph build [paths...] [options]
Terminal window
$ manni graph build -c test/graph/fixtures/dd/docs.config.yaml
Wrote .tmp/dd/graph.ttl (3 docs, 157 triples, 3 concepts)

A page piped in with - joins the graph beside the named paths:

Terminal window
$ cat draft.md | manni graph build - docs/ --as markdown
Wrote graph/graph.ttl (4 docs, 152 triples, 3 concepts)

When the terms derive source reads a termbase, the summary says how many concepts it gave:

Terminal window
$ manni graph build
Wrote graph.ttl (6 docs, 110 triples, 3 concepts (2 from the termbase))

When the build holds strategy records, the summary counts them last. One record reads 1 strategy record, and a build with none leaves the clause out:

Terminal window
$ manni graph build -o .manni/graph/internal/graph.ttl
Wrote .manni/graph/internal/graph.ttl (344 docs, 23385 triples, 560 concepts (50 from the termbase), 82 strategy records)

A build carries author-facing content by default. That is the strategy records, typed, and every value a page’s schemas mark x-manni-location: external, at any depth. --no-authoring leaves both out for one run, and graph.authoring sets the default. Publish the graph says what each derive source loses.

Warnings go to stderr, so stdout stays the one-line summary. A degraded build is still a successful one. A repository git cannot read warns once and builds the rest. A near-miss frontmatter key warns and derives nothing. Neither changes the exit code.

Each page’s parsed body is cached in .manni/graph/analyze-cache/ under the working directory, one file per page. A page is parsed again only when its bytes, its path, its --as format or the manni version changed. Links resolve against the current pages on every run, so the cache never changes the Turtle. A damaged entry is parsed afresh, and deleting the directory is always safe. The graph check in manni check reads the same cache.

ArgumentDescription
[paths...]Files, directories, or globs to build from, relative to the working directory. - reads stdin as well. Optional; without them, the configured collections.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml. See Common conventions.
--no-confign/aoffIgnore any discovered config file.
--collection<name>every collectionRead only this configured collection. Repeatable.
--exclude<glob>n/aGlob to exclude. Repeatable.
--as<format>by extensionParse every input as asciidoc, html, markdown, mdx, rst or xml. Required with -.
--ext<list>.md,.markdown,.mdx,.html,.htm,.flsnp,.adoc,.asciidoc,.rst,.dita,.ditamap,.fltoc,.flbrsExtensions for directory walks and globs, comma-separated.
--allow-emptyn/aoffZero matched files writes an empty graph and exits 0.
-o, --out<path>config out, else graph/graph.ttlWhere to write the Turtle. The directory is created if it does not exist.
--no-authoringn/aconfig authoring, else onLeave out author-facing content: strategy records, and every value a page’s schemas mark x-manni-location: external.

Validates the built graph against SHACL shapes. This is where the emergent failures live: they exist only after N documents merge into shared nodes, so no per-file check can see them. Findings carry the family’s notice | warning | error scale, and an error exits 1.

Terminal window
manni graph check [options]
Terminal window
$ manni graph check -c test/graph/fixtures/dd/violations.config.yaml
error: violations/alpha.md, violations/beta.md, violations/gamma.md: "Alpha" and "Beta" form a broader cycle, so each is its own ancestor. Remove one `broader` or `narrower` value on the cycle.
at https://example.com/dd/concept/alpha skos:broader
manni:graph-shapes:1.3.0 Concept-broader: A more general concept. A concept is never its own ancestor.
Guide: https://hawkeyexl.github.io/manni/graph/fix/
error: violations/alpha.md, violations/beta.md, violations/gamma.md: A concept is related to a concept that is also broader than it. Remove one link: from `related-concepts` or `related-terms`, or from `broader` or `narrower`.
at https://example.com/dd/concept/alpha skos:related
manni:graph-shapes:1.3.0 Concept-related: A concept on the same level as this one. It is never also an ancestor or a descendant.
Guide: https://hawkeyexl.github.io/manni/graph/fix/
error: violations/alpha.md: The page lists a variant in both `applies-to` and `not-applicable-to`. Keep it only in the one that is true.
at https://example.com/dd/doc/violations/alpha.md graph:notApplicableToVariant
manni:graph-shapes:1.3.0 Document-notApplicableToVariant: The product variants the page does not apply to. A variant cannot be in both lists.
Guide: https://hawkeyexl.github.io/manni/graph/fix/
3 errors, 0 warnings, 0 notices

Each finding opens with its severity and the pages it came from, which are where an author edits. (no page) stands in when a finding names none. The message names the frontmatter key to change. The lines under it are dim.

LineWhat it says
at <focus> <path>The graph node that failed and the property, with known prefixes compacted. A finding with no property shows the node alone.
<id> <rule>: <description>The shapes file’s id, the rule that judged the finding, and what the rule is for.
Guide: <url>The page that explains how to fix it.

The last two print only when a shape judged the finding. A sample-question finding has no shape, so only its at line follows it.

-f json prints the same findings. Each one ends with guidance, the last two lines as an object, or null when no shape judged it.

{
"severity": "error",
"shaclSeverity": "violation",
"message": "The page lists a variant in both `applies-to` and `not-applicable-to`. Keep it only in the one that is true.",
"focusNode": "https://example.com/dd/doc/violations/alpha.md",
"path": "https://hawkeyexl.github.io/manni/graph/ns#notApplicableToVariant",
"docs": ["violations/alpha.md"],
"guidance": {
"id": "manni:graph-shapes:1.3.0",
"rule": "Document-notApplicableToVariant",
"description": "The product variants the page does not apply to. A variant cannot be in both lists.",
"guide": "https://hawkeyexl.github.io/manni/graph/fix/"
}
}

-f github writes one workflow annotation per finding, on the first page it names. The guidance lines follow the message, each after %0A.

-f sarif writes one SARIF 2.1.0 run whose driver is manni-graph. A result’s ruleId is manni:graph/<rule>, or manni:graph/shacl when no shape judged it. Its locations are the pages it names, each at startLine 1. Its message.text is the message and the guidance lines, and properties.guidance holds the object. A rule carries the shape’s description and guide as fullDescription and helpUri. A finding that names no page is left out of the results. invocations[0].toolExecutionNotifications counts them instead, as 1 finding names no page. Run manni graph check to see them. The exit code is the same in every format.

A shapes file named by --shapes speaks to authors the same way when it declares the parts. Its owl:Ontology header gives the id through dcterms:identifier and the guide through rdfs:seeAlso. A file with no header is named by its path, as given. See Govern the graph for writing one, and graph shapes for reading one.

The bundled shapes are closed, so a predicate the shapes have not learned fails here. That is deliberate: a change to what the graph contains is not done until the shapes say so.

graph check also asks each page’s sample-questions through the retrieval graph search uses. A page outside the top check.questions.top results for its own question is a finding at check.questions.severity, warning by default. It names the question, the page’s rank, and the page found first.

Terminal window
$ manni graph check
warning: docs/limits.md: "How do I retry a failed upload?" does not find this page in the top 1 (it ranks 2; first is docs/retry.md)
at https://example.com/graph/doc/docs/limits.md graph:sampleQuestion

A question that finds the page nowhere says it is not found in place of the rank. The check measures whether a reader finds the page, not whether the page answers. Configuration sets the cutoff and the severity.

The questions are read from each page’s source, so a question kept out of the published graph is still asked. A page in the graph that is no longer on disk is skipped, and one warning names how many and says to rebuild.

A question can pin the body lines that answer it, as manni:ai-context:1.2.0 describes. A pinned question must find the part of the page that holds those lines. That is a section whose own text covers them, from its heading to the next heading of any rank. Text before the first heading belongs to the page itself. A hit on another section of the same page does not count.

Terminal window
$ manni graph check
warning: docs/retry.md: "How many times does an upload retry by default?" does not find the section that answers it, § Retry limits, in the top 1 (it ranks 2; first is docs/retry.md § Retry a failed upload)
at https://example.com/graph/doc/docs/retry.md graph:sampleQuestion

A pin with an integrity is compared with the page as it reads now, under the hashing rule citations use. A pin with none is checked where it points.

FindingSeverityWhat it means
The answer movednoticeThe pinned lines sit elsewhere now, and are asked there. Update the pin’s lines.
The answer moved to two placeswarningMore than one place matches, and either counts as found. Pin one of them by hand.
The answer changedcheck.questions.severityNo lines match the pin, so the question counts as not found. Re-pin it and review it.
Terminal window
$ manni graph check
notice: docs/upload.md: the answer to "How large may an upload be?" moved from lines 4-5 to 7-8, so manni graph fill --fields sample-questions re-pins it.
at https://example.com/graph/doc/docs/upload.md graph:sampleQuestion

After a rewrite that drops the answer, the same pin reads:

Terminal window
warning: docs/upload.md: lines 4-5 no longer hold the answer to "How large may an upload be?" as pinned, so re-pair it with manni graph fill --fields sample-questions and review it.
at https://example.com/graph/doc/docs/upload.md graph:sampleQuestion

A question marked reviewed: false is a draft. Its findings report at notice, whatever check.questions.severity says, and end (drafted, not reviewed). A pin marked reviewed: false reports the same way and ends (answer not reviewed). While every pin on a confirmed question is a draft, the question must also find its page at full severity. off silences every finding at check.questions.severity, drafts included. A moved answer is still reported.

A question that needs two pages, or that the docs don’t answer, lives in a golden-set file. graph.questionFiles names them, relative to the config file. Each item is a page item’s object form, with two more keys. answers[].file names the page that holds each answer, and language is optional.

questions.yaml
- question: After rotating the key, how do I check nothing broke?
answers:
- file: docs/rotate.md
lines: 4
integrity: sha256-57f8…
- file: docs/validate.md
lines: 4
integrity: sha256-7e32…
- question: Does manni run on Windows on ARM?
answers: []

Each answer must be found, as a page’s pin must. An empty answers list marks a known gap. A gap is counted and never asked, because search has no score floor to test “finds nothing” against. A finding on a file question points at the file, and its at line names the line its item starts on.

Terminal window
$ manni graph check
warning: questions.yaml: "After rotating the key, how do I check nothing broke?" does not find docs/rotate.md § Rotate in the top 1 (it ranks 4; first is docs/validate.md § Check the result)
at questions.yaml:3

Across pages and files, check also reports these.

FindingSeverityWhat it means
An answer’s page is gonecheck.questions.severityThe file an answer names does not exist. The question counts as not found.
A question asked twicecheck.questions.severityThe same question is asked elsewhere, ignoring case, punctuation and spacing. Each copy after the first, in path order, is reported.
A question copies its answernoticeOver half its adjacent word pairs also appear in its answer’s lines. It tests matching words more than finding.
The found rate is lowerrorFewer reviewed questions are found than check.questions.foundThreshold asks for. check.questions.severity does not touch it.
Terminal window
warning: questions.yaml: docs/old.md, which holds the answer to "How large may an upload be?", does not exist, so re-pin or remove the question.
at questions.yaml:1
warning: docs/retry.md: "how do I retry a failed upload" is also asked on docs/limits.md, so keep it where it is answered.
at https://example.com/graph/doc/docs/retry.md graph:sampleQuestion
notice: docs/retry.md: "Do uploads retry three times by default?" repeats 5 of its 6 word pairs from its answer, so it tests matching words more than finding. (drafted, not reviewed)
at https://example.com/graph/doc/docs/retry.md graph:sampleQuestion
error: (no page): 0 of 1 reviewed questions are found in the top 1 (0%), under the 98% that graph.check.questions.foundThreshold asks for.
at questions

A golden-set file that cannot be read stops the run with exit 2.

Casestderr
A listed file is missingmanni: graph.questionFiles names questions.yaml, which does not exist. Fix the path, or remove it from graph.questionFiles.
A file fails its schemamanni: questions.yaml: /0/answers/0 must have required property 'lines'.

When any question exists, check prints a summary of the whole set between the findings and the counts.

Terminal window
$ manni graph check
questions 3: 2 reviewed, 1 drafted, on 1 page
found 2 of 2 reviewed in the top 2 (100%), 1 first (50%)
drafted 1 of 1 in the top 2, not counted until reviewed
answers 3 pinned, 1 not reviewed: 3 current, 0 moved, 0 changed; 0 not pinned
0 errors, 0 warnings, 1 notice
RowWhat it counts
questionsEvery question, reviewed and drafted, and where they are declared.
foundReviewed questions with a test, a page or at least one pin. A question is found when every answer is in the top results, and first when every answer ranks first.
draftedThe same count for drafts. It prints only when a draft has a test, and the rate gate ignores it.
answersEvery pin, how many are drafts, and how each compares with its page now. not pinned counts page questions with no pin.
gapsGolden-set questions with no answers. It prints only when there is one.

A changed answer, or one whose page is gone, counts as not found. The percentages round down. Under -f github the summary prints after the annotations, as plain text. The JSON report gains a questions key beside findings, with the same counts.

"questions": {
"total": 2, "reviewed": 2, "drafted": 0, "pages": 1,
"files": ["questions.yaml"], "top": 1,
"found": { "reviewed": { "inTop": 1, "first": 1, "of": 1 },
"drafted": { "inTop": 0, "first": 0, "of": 0 } },
"answers": { "pinned": 0, "unreviewed": 0, "current": 0, "moved": 0, "changed": 0, "unpinned": 1 },
"gaps": 1
}

Stored per CI run, that object is the time series. manni keeps no history of its own.

When a chunk artifact sits beside the graph, check also asks each reviewed question through graph retrieve, in the question’s own language. A question is cited when the bundle cites a node that answers it. For a question with no pin, that is its page or any of its sections. For a pinned one, it is a node whose own text holds each answer.

Terminal window
$ manni graph publish && manni graph check
questions 4: 4 reviewed, 0 drafted, on 4 pages
found 3 of 4 reviewed in the top 5 (75%), 3 first (75%)
cited 2 of 4 reviewed by graph retrieve (50%), 1 withheld
answers 0 pinned, 0 not reviewed: 0 current, 0 moved, 0 changed; 4 not pinned

The JSON questions object gains cited and withheld, both counts of reviewed questions. The row’s denominator is found.reviewed.of.

FindingSeverityMessage
Not citedquestions.severity"<question>" is not cited by graph retrieve, which cites <page> § <section> first.
Not cited, withheldquestions.severity"<question>" is not cited by graph retrieve, because graph.retrieve.withhold left out § <section> as drifted.
Cited outside its languageerror"<question>" asks in de, and graph retrieve cites <page>, which is en.
Under citedThresholderror2 of 4 reviewed questions are cited by graph retrieve (50%), under the 75% that graph.check.questions.citedThreshold asks for.
No chunk artifactnoticethe cited row did not run: no chunk artifact; run manni graph publish.
An artifact from another graphnoticethe cited row did not run: graph/chunks.json was built from another graph; run manni graph publish.

A withheld answer that is a whole page is named by its path. When the bundle cites nothing, the first message ends which cites nothing for it. The row never runs without an artifact, so citedThreshold gates only a published graph.

OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file to validate.
--shapes<path>config check.shapes, else bundledA shapes .ttl file. Repeatable, one path per occurrence, never comma-separated. Given here or in config, these replace the bundled shapes rather than adding to them. Include the bundled file when you mean to extend it.
-f, --format<pretty|json|github|sarif>prettyOutput format. github writes one workflow annotation per finding, and sarif writes one SARIF 2.1.0 log.

There is no graph validate. A graph: block is frontmatter, and frontmatter is checked by manni meta validate against the page vocabulary. That is the command every vocabulary in this family is checked with.

The page vocabulary is manni:graph:1.0.0, a built-in schema of manni meta. Name it for the pages it governs:

manni.config.yaml
meta:
overrides:
- collection: site
schemas:
- manni:graph:1.0.0
Terminal window
$ manni meta validate
✗ docs/install.md
/graph must NOT have additional property 'bogus' (line 3) [manni:graph:1.0.0]
manni:graph:1.0.0 graph
Guide: https://hawkeyexl.github.io/manni/meta/reference/schemas/manni-graph/
1 file checked, 0 passed, 1 failed, 1 error

A typo inside the block is a schema error with a line number, because the block is closed. manni graph build reads x-manni-graph-output from the set manni meta validate resolves for each page, to decide what a published graph may carry.

Lists the SHACL node shapes graph check runs, grouped by the file they come from, with what each one targets. It reads the same shapes check does: the --shapes files, else graph.check.shapes, else the bundled file. It reads no documents and no graph, and it exits 0.

Terminal window
manni graph shapes [options]
Terminal window
$ manni graph shapes
manni:graph-shapes:1.3.0 (bundled)
Guide: https://hawkeyexl.github.io/manni/graph/fix/
ActivityShape prov:Activity A generation, fill or build run that manni records. manni writes it.
AssociationShape prov:Association The role an agent played in an activity. manni writes it.
AttributionShape prov:Attribution The role an author played on a page. manni writes it.
AudienceShape schema:Audience An audience from the content strategy, one per audience record.
BlockShape graph:Block A paragraph, list or element that an inline meta statement describes.
ConceptShape skos:Concept A concept of the glossary, named by a term page, a graph label, a tag or a concepts entry.
DocumentShape graph:Document A page of the docset, as its frontmatter, links, headings and history describe it.
FilledFieldEntryShape objects of graph:filledFieldEntry One field a graph fill run filled, with the model's confidence. manni writes it from meta-provenance.
JourneyShape graph:Journey A journey from the content strategy, one per journey record.
OrganizationShape prov:Organization An organization credited on a page.
PersonShape prov:Person A person credited as an author of a page.
PersonaShape schema:PeopleAudience A persona from the content strategy, one per persona record.
ProductVariantShape iirds:ProductVariant A product variant that pages name in applies-to or not-applicable-to.
SchemeShape skos:ConceptScheme The glossary that holds every concept of the docset. manni writes it.
SectionShape graph:Section A heading of a page, typed through graph.sections or a meta statement on the heading.
SoftwareAgentShape prov:SoftwareAgent A model or tool that wrote or filled part of a page, or built the graph.
VerificationShape graph:Verification One verified-by entry: a tool, and the check it runs.

Each file opens with its id and where it came from: (bundled), or the path as given. A file with no owl:Ontology header is named by that path. The Guide: line prints when the header names one.

-f json prints { shapes }, one object per node shape:

{
"shapes": [
{
"name": "DescribedShape",
"id": "house:shapes:1.0.0",
"source": "house-shapes.ttl",
"target": "https://hawkeyexl.github.io/manni/graph/ns#Document",
"closed": false,
"description": "A page the house style describes.",
"guide": "https://example.com/house/guide/"
}
]
}
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
--shapes<path>config check.shapes, else bundledA shapes .ttl file. Repeatable, one path per occurrence, never comma-separated.
-f, --format<pretty|json>prettyOutput format.

Prints every property one node shape checks, and the frontmatter key that feeds it. The shape is named by its local name, as graph shapes lists it.

Terminal window
manni graph shapes show <shape> [options]
Terminal window
$ manni graph shapes show ConceptShape
ConceptShape manni:graph-shapes:1.3.0 skos:Concept, closed
A concept of the glossary, named by a term page, a graph label, a tag or a concepts entry.
Guide: https://hawkeyexl.github.io/manni/graph/fix/
Concept-altLabel skos:altLabel graph.alt-labels · optional, repeats
Other names readers use for the concept. Search matches them too.
Concept-broadMatch skos:broadMatch optional, repeats
A broader concept in another vocabulary, from the term page. It never points into this glossary.
Concept-broader skos:broader graph.broader · optional, repeats
A more general concept. A concept is never its own ancestor.
Concept-closeMatch skos:closeMatch optional, repeats
A near-equivalent concept in another vocabulary, from the term page. It never points into this glossary.
Concept-definition skos:definition optional, repeats
What the concept means, from its term page.
Concept-exactMatch skos:exactMatch optional, repeats
The same concept in another vocabulary, from the term page. It never points into this glossary.
Concept-hiddenLabel skos:hiddenLabel optional, repeats
Misspellings and old names that search matches but never shows, from the term page.
Concept-inScheme skos:inScheme required, repeats
The glossary the concept belongs to. manni writes it.
Concept-narrowMatch skos:narrowMatch optional, repeats
A narrower concept in another vocabulary, from the term page. It never points into this glossary.
Concept-narrower skos:narrower graph.narrower · optional, repeats
A more specific concept. manni also writes it as the inverse of broader.
Concept-notation skos:notation optional, repeats
A code that names the concept, from its term page.
Concept-prefLabel skos:prefLabel graph.label · required, repeats
The concept's name. Every concept has one.
Concept-prefLabel-collision skos:prefLabel optional, repeats, warning
One name per language, so search and the glossary agree on a spelling.
Concept-related skos:related graph.related-concepts · optional, repeats
A concept on the same level as this one. It is never also an ancestor or a descendant.
Concept-relatedMatch skos:relatedMatch optional, repeats
A related concept in another vocabulary, from the term page. It never points into this glossary.
Concept-scopeNote skos:scopeNote optional, repeats
When to use the concept, and when not to, from its term page.
Concept-topConceptOf skos:topConceptOf optional, repeats
Marks a concept with no broader concept. manni writes it.

Each row is a rule, the property it checks, and the frontmatter key that feeds it when an author writes one. A row with no key is a value manni writes. A severity other than error closes the row. The indented line under it is the rule’s description, from sh:description.

-f markdown prints a frontmatter skeleton of the keys an author writes. Each key follows its description as a comment. An optional key is commented out, and graph.* keys nest under graph:.

Terminal window
$ manni graph shapes show ConceptShape -f markdown
---
# ConceptShape, manni:graph-shapes:1.3.0
# Guide: https://hawkeyexl.github.io/manni/graph/fix/
graph:
# Other names readers use for the concept. Search matches them too.
# alt-labels: []
# A more general concept. A concept is never its own ancestor.
# broader: []
# The concept's name. Every concept has one.
label: []
# A more specific concept. manni also writes it as the inverse of broader.
# narrower: []
# A concept on the same level as this one. It is never also an ancestor or a descendant.
# related-concepts: []
---

-f json prints { shapes } with the one shape, which gains properties. Each property is { rule, path, key, min, max, severity, description }. key is null for a value manni writes, and max is null when the property repeats.

ArgumentDescription
<shape>The node shape to show, by local name, such as DocumentShape.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
--shapes<path>config check.shapes, else bundledA shapes .ttl file. Repeatable, one path per occurrence, never comma-separated.
-f, --format<pretty|json|markdown>prettyOutput format.

Each of these exits 2.

Casestderr
No shape namederror: missing required argument 'shape'
A shape the files do not holdmanni: Unknown shape "Document". Available: ActivityShape, AssociationShape, AttributionShape, BlockShape, ConceptShape, DocumentShape, FilledFieldEntryShape, OrganizationShape, PersonShape, ProductVariantShape, SchemeShape, SectionShape, SoftwareAgentShape, VerificationShape.
A shape named when the files define no node shapesmanni: Unknown shape "Document". The shapes in effect define no node shapes.
-f markdown on a shape with no frontmatter keysmanni: ActivityShape has no frontmatter keys. manni writes every value it checks.
-f markdown on graph shapesmanni: Unknown --format "markdown". Use pretty | json.
An unknown -f on graph shapes showmanni: Unknown --format "xml". Use pretty | json | markdown.
A --shapes file that does not existmanni: Shapes file not found: <resolved path>. Fix the path in --shapes or graph.check.shapes. Leave both unset to use the bundled shapes.

Proposes graph metadata fields and sample questions from each page’s content, and writes the ones that clear a confidence gate. This is the one verb that reaches a model.

Terminal window
manni graph fill [paths...] [options]

Run --dry-run first. The proposals are cached before the gate, so re-running at a different --confidence spends no turns. A turn is one inference call, one page is one turn, and a cached page costs none. A page the budget stopped at is reported skipped with the reason turn budget.

With -, the page from stdin is filled in memory and printed to stdout, as meta fill - does. The report goes to stderr in either format.

A field already held in an external-metadata manifest counts as filled. Each key is written where meta fill would write it. A key a manifest owns goes to the page’s entry there, and every other key goes to the page. A page that carries graph: while a manifest owns it is that page’s error, in the words meta validate uses for the collision.

A key that goes to the page is written in the page’s own format, where meta fill writes it. Markdown and MDX get YAML frontmatter. HTML gets one <meta name="graph"> in <head>, the block as one line of YAML flow. A DITA topic gets an <othermeta name="graph"> in its <prolog>. A fenced AsciiDoc or reStructuredText page has its fence edited in place. meta-provenance goes the same way. Two kinds of page cannot take a write: an AsciiDoc or reStructuredText page with no fenced block, and an HTML page with no <head>. Each is that page’s error, in meta fill’s words, and is left unchanged. The run exits 1. A page whose manifest owns every key written is not refused. See the formats reference.

The graph check vets each proposal against the whole configured corpus. A corpus page it cannot read is left out, and one warning lists it. A named page that cannot be read is that page’s error.

Every written graph value is recorded in the page’s meta-provenance, naming the model and the fields. See Fill the vocabulary.

sample-questions is one of the fill fields, and it is in the default set. A config that lists fill.fields without it leaves questions alone. Questions run in a pass of their own beside the graph fields, in this order:

  1. Re-pin. An answer whose lines moved gets its new lines. A pin with lines and no integrity gets one. This needs no model and spends no turn.
  2. Pair. For a question with no answers, the model quotes the passage that answers it. fill finds the quote in the body and pins the whole lines it touches. The quote itself is never written.
  3. Re-pair. An answer whose lines changed gets a new pin the same way.
  4. Draft. A page with fewer than fill.questions.perPage questions gets new ones until it has that many.

A new pin is written reviewed: false, and a paired question keeps its own standing. A drafted question is written reviewed: false, with generated-by and confidence when fill.writeProvenance is on. Questions never touch meta-provenance, and --force never rewrites or removes one. A page with enough questions, each one paired and none moved or changed, is complete.

The gates run in this order. A draft that fails one is dropped, and an existing question that fails one is not-paired and kept.

GateReason shown
The quote is not in the page.answer not on page
A draft repeats a question asked on any page or golden-set file.duplicate of <file>
A draft repeats over half its word pairs from its answer.copies its answer (5 of 6 word pairs)
The model finds no passage for an existing question.no passage answers it
The confidence is under --confidence.low confidence, not written

A question request sends the whole body. A body over --chunk-chars is split at line boundaries, one request per piece. Drafts from every piece are pooled, and the most confident are kept. A golden-set file named in questionFiles gets the re-pin step only. On HTML and DITA, the questions sit inside the body that lines are counted from, so every pin is taken from the page as written.

Pretty output adds one line per question under its page:

filled docs/retry.md (sample-questions)
re-pinned "How do I resume a paused upload?" lines 9 to 12
paired "How do I retry a failed upload?" lines 4 (0.77)
drafted "How often does the client try again?" lines 8 (0.90)
dropped "Open the upload list and choose retry?" copies its answer (6 of 6 word pairs)
filled questions.yaml (sample-questions)
re-pinned "After rotating the key, how do I check nothing broke?" docs/rotate.md lines 4 to 6

Under -f json, each result gains a questions list. Each entry holds action, question, and any of file, lines, from, confidence and reason. The action is drafted, paired, re-paired, re-pinned, dropped or not-paired.

--offline, or fill.offline in config, sends nothing off this machine. It means what it means for meta fill and term fill.

  • auto resolves only to llama-cpp, and only to a model already downloaded. Nothing is downloaded.
  • A configured hosted provider is set aside, and one notice says so: manni: --offline: using llama-cpp instead of "anthropic" from graph.provider.
  • A hosted provider named with --provider is a usage error, exit 2.
  • With a downloaded model, the run is the same as under --local.
  • With none, only the re-pin step runs. Each page that needed a model is skipped with the reason offline, and the run exits 0 with one warning.
$ manni graph fill --fields sample-questions --offline
manni: no model is available offline, so only answer pins were updated, and nothing was drafted or proposed; download one first with --local, or drop --offline.
filled docs/retry.md (sample-questions)
re-pinned "How do I resume a paused upload?" lines 9 to 12
skipped docs/limits.md (offline)

The warning is on stderr and the report on stdout.

ArgumentDescription
[paths...]Files, directories, or globs to fill, relative to the working directory. - reads stdin as well. Optional; without them, the configured collections.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
--collection<name>every collectionRead only this configured collection. Repeatable.
--exclude<glob>n/aGlob to exclude. Repeatable.
--as<format>by extensionParse every input as asciidoc, html, markdown, mdx, rst or xml. Required with -.
--ext<list>.md,.markdown,.mdx,.html,.htm,.flsnp,.adoc,.asciidoc,.rst,.dita,.ditamap,.fltoc,.flbrsExtensions for directory walks and globs, comma-separated.
--allow-emptyn/aoffZero matched files is an empty report and exit 0.
-f, --format<pretty|json>prettyOutput format.
--dry-runn/aoffReport proposals without writing files.
--forcen/aoffOverwrite graph fields a human set. Without it, a field already present on the page is left alone. It never rewrites or removes a sample question.
--no-cachen/aoffBypass the proposal cache, so every page is asked again.
--no-validate-graphn/aoffSkip the SHACL guardrail that vets each proposal against the derived graph before it is written.
--sectionsn/aconfig fill.sections, else offAlso propose per-section metadata, not only page-level.
--max-turns<n>config fill.maxTurns, else unboundedStop after this many inference calls. A whole number, at least 1.
--confidence<n>config fill.confidenceThreshold, else 0.7Minimum model confidence, 0 to 1, for a field to be written. Outside that range is a usage error.
--fields<list>config fill.fields, else every fieldWhich fields to propose, comma-separated and given once. sample-questions is one of them. An unknown name is a usage error naming the allowed set.
--chunk-chars<n>config fill.chunkChars, else 12000The most characters of a page one question request sends. A whole number, at least 1.
--provider<name>config graph.provider, then providers.provider, then autoauto, anthropic, openai, claude-cli, or llama-cpp. An unknown name is exit 2.
--model<model>config graph.model, else the provider’s defaultThe model within the provider. It needs a named provider, from here or from config, because a model name does not say which provider owns it.
--localn/aoffRun inference on this machine with llama-cpp, over graph.provider and the family’s providers.provider.
--offlinen/aconfig fill.offline, else offSend nothing off this machine. Only a local model already downloaded runs. See Offline.

Matches a triple pattern against the built graph. Omit a term and it is a wildcard, so all three omitted prints the whole graph.

Terminal window
manni graph query [options]
Terminal window
$ manni graph query -c test/graph/fixtures/dd/docs.config.yaml --predicate dcterms:subject
<https://example.com/dd/doc/docs/configuration.md> dcterms:subject <https://example.com/dd/concept/reference>
<https://example.com/dd/doc/docs/getting-started.md> dcterms:subject <https://example.com/dd/concept/onboarding>
2 match(es)

The three pattern flags are long-only and named for the triple position each one fills. -o is --out on every other verb, and -s is the metadata tool’s schema flag. One letter meaning two things across one family costs more than the letters it saves.

OptionArgumentDefaultDescription
--subject<term>wildcardThe subject, as a full IRI or a prefix:local name.
--predicate<term>wildcardThe predicate, as a full IRI or a prefix:local name.
--object<term>wildcardThe object, as a full IRI, a prefix:local name, or a literal value.
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file to read.
-f, --format<pretty|json>prettyOutput format.

No match is exit 0 with No matches., not a finding. A query that asks a question and gets no answer has not failed.

Summarizes the graph. It reports node counts, the most connected documents, orphans, broken internal links and section references, and per-field metadata coverage.

Terminal window
manni graph stats [options]
Terminal window
$ manni graph stats -c test/graph/fixtures/dd/docs.config.yaml
Triples: 157
Documents: 3
Sections: 8
Concepts: 3
References: 2
Most connected:
docs/configuration.md (2)
docs/getting-started.md (2)
Orphan docs (1):
docs/troubleshooting.md
Broken internal links (1):
docs/troubleshooting.md -> missing.md

The sample-questions coverage row counts the pages that ask a reader question. It is gated only when the per-field coverageThreshold map names it, never by a single number. The report ends with the concepts no question reaches. A concept has a question when a page whose primary topic or subjects name it asks one. The list is reported and never gated, like orphans. A graph with no concepts prints no such list.

Terminal window
$ manni graph stats -c test/graph/fixtures/dd/docs.config.yaml
…
Concepts with no question (3):
Installation
onboarding
reference

The JSON report lists those concepts by IRI, under conceptsWithoutQuestions.

OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file to read.
-f, --format<pretty|json>prettyOutput format.
--checkn/aoffExit 1 when a broken internal link exists, or when coverage is below its threshold. This is the CI gate.
--top<n>every hubHow many most-connected documents to list. A whole number, at least 1.
--coverage-threshold<pct>config stats.coverageThreshold, else noneMinimum coverage percentage, 0 to 100, applied to every measured field but sample-questions for this run. Outside that range is a usage error. See Coverage.

Ranks graph nodes for a text query. It reads the search index that manni graph export search writes, and adds a vector leg when manni graph embed has written sidecars.

Terminal window
manni graph search <query> [options]
Terminal window
$ manni graph search "installation" -c test/graph/fixtures/dd/docs.config.yaml
0.0164 https://example.com/dd/concept/installation [skos:Concept] — Installation
1 result (lexical)
ArgumentDescription
<query>The text to rank nodes against. Required.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file the index sits beside.
-i, --index<dir>beside the graphDirectory holding the indexes and the manifest.
--lang<tag>the only localizationWhich localization to search, matched in any case. Required when the corpus has more than one.
--limit<n>10Maximum results. A whole number, at least 1.
--vectors<path>from the manifestVector sidecar path.
--mode<mode>hybrid when vectors exist, else lexicalWhich legs to run. An unknown mode is a usage error naming the allowed set.
-f, --format<pretty|json>prettyOutput format.

Walks the graph outward from a node and reports what it reaches, with the full trace of why each hop was taken. This is the impact question: what depends on this page, through a hyperlink or through a shared concept.

Terminal window
manni graph traverse <node> [options]
Terminal window
$ manni graph traverse -c test/graph/fixtures/dd/docs.config.yaml \
https://example.com/dd/doc/docs/getting-started.md --impact
https://example.com/dd/doc/docs/configuration.md — Configuration
https://example.com/dd/activity/build
https://example.com/dd/graph
3 nodes, 5 hops, 0 excluded
ArgumentDescription
<node>Where to start, as a full IRI or a prefix:local CURIE. Required.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file to read.
-d, --depth<n>1; 3 under --impactMaximum hops from the node. A whole number, and 0 is allowed: it means the node itself, which is a real answer.
--predicates<list>every predicateOnly follow these predicates, comma-separated and given once. A CURIE holds no comma, so one separator is enough. Naming rdf:type follows type edges. A predicate no edge uses is exit 2.
--reversen/aoffFollow inbound edges, to find who points at this node.
--impactn/aoffTransitive inbound reach, to find what a change here affects.
--variant<variant>no filterScope filter on a product variant IRI, title, or slug.
--software-subject<subject>no filterScope filter on a software subject. An unknown one is exit 2.
--lang<tag>no filterScope filter on a BCP-47 language tag, matched in any case.
--limit<n>unboundedStop after this many nodes. A whole number, at least 1.
--contextn/aoffReturn the passages the walk reaches, with their text and proof, as a bundle. It reads the chunk artifact beside the graph.
--max-chars<n>config retrieve.maxCharsThe character budget for the passages. Valid only with --context.
--withhold<list>noneAlso leave out passages that are drifted or failing, comma-separated and given once. Valid only with --context.
--where<field=value>noneServe only passages whose row metadata field holds the value, as graph retrieve does. Repeatable, and every condition must hold. Valid only with --context.
-f, --format<pretty|json>prettyOutput format.

Without --context, the output is the walk above, unchanged. With it, the node the walk starts from has the role match. A node reached outward is context, and one reached under --impact or --reverse is dependent.

Terminal window
$ manni graph traverse urn:manni:graph:doc/docs/en/rotate.md --context
rotate.md match
rotate.md § Rotate the encryption key context, depth 1
setup.md context, depth 1
3 passages, in en. Pass -f json for the text and trace.

--max-chars, --withhold or --where without --context is exit 2, with manni: --max-chars needs --context., and likewise for the other two. A malformed --where is exit 2, with the message graph retrieve prints.

Answers a question from the chunk artifact. It takes only the question and where the artifact is. Every retrieval setting comes from graph.retrieve.

Terminal window
manni graph lookup <question> [options]

It runs three operations and returns each one in full:

  • Matches. Every ranked match, with its lexical, vector and fused scores. The top retrieve.seeds passages are the matches.
  • Traversal. Every passage reached outward from the matches, to retrieve.depth hops, with every edge it followed.
  • Impact. Every passage that depends on the matches, three hops inward, with every edge.

It enters every localization and keeps the one whose best match scores highest, so it never asks for a language. The lexical score decides, since the words a question shares with a page say which language it is in. The question is then embedded with that language’s model alone. Nothing is dropped from a section. The budget, retrieve.maxChars, applies to each section’s text on its own. A passage past it keeps its node, edges and record, and its text is null.

Terminal window
$ manni graph lookup "How do I rotate the encryption key?"
Matches
1 rotate.md lexical 134.9, vector 0.09
2 ci.md § Run in CI lexical 60.5, vector 0.14
3 rotate.md § Rotate the encryption key lexical 170.5, vector -0.49
Traversal, depth 1
setup.md from #1 via dcterms:references
rotate.md § Rotate the key with the CLI from #3 via dcterms:hasPart
rotate.md § What rotation re-encrypts from #3 via dcterms:hasPart
Impact, depth 3
ci.md depth 1, hasPart #2
release.md depth 2, references ci.md
3 matches, 3 reached outward, 2 depend on them, in en. Pass -f json for the text, edges and trace.

Under each passage, the pretty output adds its proof when there is any. That means a pins line, a dates line and an evals line.

ArgumentDescription
<question>The question, in quotes. Required.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file. The artifact must have been built from it.
-i, --index<dir>beside the graphThe directory holding chunks.json, its rows and the search index.
-f, --format<pretty|json>prettyOutput format. json prints the lookup result.
KeyMeaning
question, languageThe question, and the language chosen.
matches.passages[]Every match in rank order: iri, title, page, lines, rank, score, text, truncated and provenance. score is { lexical, vector, fused }, with null where a path did not run or did not rank the passage.
traversal.passages[], impact.passages[]Every passage reached outward or inward, with depth in place of rank. A passage reached by both walks appears in both. A match never repeats in either.
traversal.edges[], impact.edges[]Every edge followed, as { from, predicate, to } in the graph’s own direction.
*.truncatedtrue when a passage in the section lost its text to the budget.
pages{ "<page>": { metadata } }: the delivered fields of each page served, once per page.
citations{ iri, title, sourceUrl } for each passage served with its text.
traceThe trace, whose entry adds languages, each language’s best score, and chosen.
refusalPresent only when nothing matched, or every match was withheld. Every section is then empty.

A passage in passages is a passage the chunk artifact holds. A node with no passage, such as a concept, appears only in edges.

matches plus traversal is what graph retrieve returns for the chosen language without a budget. impact is everything graph traverse --impact reaches from each match, less the matches.

Retrieves passages for a question, with control over language, scope, depth and budget. It prints its bundle’s rows, without lookup’s section headings.

Terminal window
manni graph retrieve <question> [options]
Terminal window
$ manni graph retrieve "How do I rotate the encryption key?" --lang en --where tags=keys
rotate.md match
rotate.md § Rotate the encryption key match
rotate.md § Rotate the key with the CLI match
rotate.md § What rotation re-encrypts context, depth 1
4 passages, in en. Pass -f json for the text and trace.

The top --seeds ranked passages are the matches. The walk goes outward from them, --depth hops, and the bundle holds what fits in --max-chars, in that order.

ArgumentDescription
<question>The question, in quotes. Required.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file. The artifact must have been built from it.
-i, --index<dir>beside the graphThe directory holding chunks.json, its rows and the search index.
-f, --format<pretty|json>prettyOutput format. json prints the bundle.
--lang<tag>the only localizationThe localization, matched in any case. Required when the corpus has more than one.
--variant<variant>no filterScope filter on a product variant IRI, title, or slug. An unknown one is exit 2.
--software-subject<subject>no filterScope filter on a software subject. An unknown one is exit 2.
--mode<mode>hybrid when the language’s rows carry vectors, else lexicalWhich legs rank the question: lexical, hybrid or vector.
--seeds<n>config retrieve.seedsHow many ranked passages the walk starts from. A whole number, at least 1.
-d, --depth<n>config retrieve.depthHops out from each match. 0 serves the matches alone.
--max-chars<n>config retrieve.maxCharsThe character budget. A passage that does not fit is left out, and truncated says so.
--withhold<list>noneAlso leave out passages that are drifted or failing, comma-separated and given once. It only tightens the floor the export applied.
--where<field=value>noneServe only passages whose row metadata field holds the value. A list field matches when it contains it. Repeatable, one condition per occurrence, and every condition must hold.

A candidate --where rejects gives its place to the next one. A withheld match keeps its place and is left out, so every match withheld is a refusal.

graph retrieve -f json and graph traverse --context -f json print a bundle.

KeyMeaning
question, nodeThe question on retrieve, or the node on traverse.
languageThe language served, or null.
context[]Each passage: iri, title, text, sourceUrl, depth, page, provenance and role. role is match, context or dependent.
citations[]{ iri, title, sourceUrl } for each passage in context.
pages{ "<page>": { metadata } }: the delivered fields of each cited page, once per page.
truncatedtrue when the budget left a passage out.
refusal{ reason, detail }, present when nothing could be served.
trace.entry{ candidates }, the ranked candidates that became matches. fallback is "lexical" when the vector path was wanted and could not run, or when the language has no model and another has one.
trace.hops, trace.exclusions, trace.resolutionsEvery edge walked, every node a scope filter excluded, and every row looked up. A candidate no row holds is a resolution with ok: false.
trace.withheld{ node, because, at } for each passage the floor left out. at is export or query. Always present.
trace.filtered{ node, field } for each passage --where left out, with the first field it failed. The vector store applies --where itself, so a row it never returns is not listed. Always present.

A refusal is an answer, and it exits 0. Its reason is one of these:

ReasonWhen
no-routeNothing matched, or nothing was reachable from the node.
no-contentEvery passage was over the budget.
withheldEvery match was withheld.
lang-requiredThe corpus has several localizations and none was named. The CLI makes this exit 2 instead. lookup never gives it.
unknown-nodeThe start node is not in the graph. The CLI makes this exit 2 instead.
unknown-scopeA variant, subject or language names nothing. The CLI makes this exit 2 instead.

Each is exit 2, on stderr.

CaseMessage
No artifactmanni: graph/chunks.json does not exist; run manni graph publish.
An artifact from another graphmanni: graph/chunks.json was built from another graph; run manni graph publish.
The query model differs from the language’s rows’manni: chunks.json was embedded with <id>, and this run loads <id>; run manni graph publish again.
Several localizations, no --langmanni: This corpus has more than one localization (de, en) — pass --lang to choose one.
A bad --withhold valuemanni: --withhold takes drifted or failing, and got stale.
A malformed --wheremanni: --where takes field=value, and got audience.

Serves the chunk artifact to an agent over the Model Context Protocol (MCP), as JSON-RPC on stdio. It serves until stdin closes, then exits 0. Serve the docs to an agent walks through registering it.

Terminal window
manni graph serve [options]
Terminal window
$ manni graph serve
manni: serving lookup, retrieve, traverse and impact over stdio from graph/chunks.json.

That line goes to stderr, because stdout carries the protocol. The server needs @modelcontextprotocol/sdk, an optional dependency npm installs with manni. It opens the artifact once, at start, and answers every call from it.

With --publish, the server keeps the artifact current itself. It answers the handshake at once and publishes behind it, so a client’s start-up timeout never waits on a publish. tools/list answers at once too.

Terminal window
$ manni graph serve --publish
manni: publishing the graph before serving it: graph/chunks.json does not exist.
manni: published the graph in 11.3s.
manni: serving lookup, retrieve, traverse and impact over stdio from graph/chunks.json.

It publishes at start when the first of these holds, and names it in the first line:

ReasonWhen
<dir>/chunks.json does not existNo artifact.
<dir>/chunks.json is not a chunk manifestThe manifest does not parse.
<dir>/chunks.json was built from another graphThe manifest and the graph disagree.
<dir>/graph.ttl does not existNo graph.
it records no commitThe manifest was built outside git.
it was built at <sha> and HEAD is <sha>HEAD moved since. Both are 7-character SHAs.
the working tree has changesgit status lists a file outside <dir>.

When none holds, it prints only the serving line.

Before each call, it compares the checkout with the one the last publish began from. That means HEAD, plus the path, size and modification time of each file git status lists. Untracked files count, and files under <dir> do not. When they differ, it publishes again before it answers:

Terminal window
manni: republishing the graph: the checkout changed since the last publish.
manni: published the graph in 9.5s.

A call made during a publish waits for it, then answers from the new artifact. An edit made during a publish starts another one on the next call. Outside git there is nothing to compare, so the server publishes once, at start. Publish’s own report never reaches stdout. Its warnings go to stderr.

OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file. The artifact must have been built from it.
-i, --index<dir>beside the graphThe directory holding chunks.json, its rows and the search index.
--withhold<list>noneAlso leave out passages that are drifted or failing on every call, comma-separated and given once. No tool argument can loosen it.
--publishn/aoffPublish the graph when its artifact is missing or older than the checkout, and again before any call made after the checkout changes. It connects at once, and calls wait for a publish in progress.
--no-authoringn/aconfig authoring, else onPublish without author-facing content, as graph publish --no-authoring does. Valid only with --publish.

With --publish, -i is the directory publish writes to, as its -o, and the one the server reads from. The server reads the graph from there too, under the file name of config out. -g is refused, because publish writes the graph into that directory.

tools/list gives four tools, in this order. Each is read-only and closed-world, and each runs the same core as its CLI twin.

ToolRequiredOptionalReturnsCLI twin
lookupquestionnonethe lookup resultgraph lookup
retrievequestionlang, variant, softwareSubject, depth, maxChars, wherethe bundlegraph retrieve
traversenodedepth, predicates, reverse, lang, variant, softwareSubject, limit, maxChars, wherethe bundlegraph traverse --context
impactnodedepth (default 3), lang, variant, softwareSubject, limit, maxChars, wherethe bundlegraph traverse --impact --context

where is an object of field to value, such as { "tags": "keys" }. A list of values asks for each one, as a repeated --where does. Every input schema sets additionalProperties: false, and none names withhold.

Each call returns its result as structuredContent, and again as one text block holding the same JSON. Each tool declares an outputSchema for it.

An answer over MCP is the CLI twin’s result, made to fit an agent’s context. A refusal goes out as it is. Every other answer is compacted first:

  • trace keeps entry, withheld and filtered. It leaves out hops, exclusions and resolutions.
  • A passage whose text is null carries no provenance.
  • pages names only the pages of passages that carry text.

The compact answer is then capped at graph.serve.maxChars characters of JSON, 40000 by default. While it runs longer, passages become references, one at a time, in this order:

ToolReduced first to last
lookupimpact passages, deepest first and later first at equal depth. Then traversal passages the same way. Then impact edges from the end, then traversal edges. Then matches, lowest-ranked first.
retrieve, traverse, impactcontext passages, last first.

A reference keeps iri, page, and whichever of title, lines, rank, score.fused, depth and role the passage has. Its text is null and its truncated is true. A section that loses text or an edge sets its own truncated to true. pages drops a page once no passage with text names it. The cap stops as soon as the answer fits, and an answer that still does not fit goes out as it is.

graph lookup -f json and the other CLI twins print the full result, and so does the runtime’s lookup.

CaseWhat the agent gets
A refusalThe result with its refusal, and isError: false.
A CLI usage error, such as an unknown node or no langA refusal with the CLI’s text in detail, and isError: false.
An argument the schema does not name, or a value of the wrong typeisError: true, with Invalid arguments for <tool>: <why>.
With --publish, a publish that failsisError: true, with graph publish stopped at <step>: <why>. Run manni graph publish to see the whole report.

When a publish fails, stderr gets manni: graph publish stopped at <step>: <why> too. A failure at start answers every call that way, and the server exits 2 when stdin closes. A later failure answers only the calls that waited on it. The previous artifact stays loaded, and the next call after another change tries again.

A usage error takes the reason that fits it. An unknown node is unknown-node, and a missing lang is lang-required. An unknown variant, subject, language or predicate is unknown-scope.

Each is exit 2, on stderr, before anything is served.

CaseMessage
No SDKmanni: graph serve needs the optional @modelcontextprotocol/sdk package; run npm install @modelcontextprotocol/sdk.
No artifact, without --publishmanni: graph/chunks.json does not exist; run manni graph publish.
An artifact from another graph, without --publishmanni: graph/chunks.json was built from another graph; run manni graph publish.
A bad --withhold valuemanni: --withhold takes drifted or failing, and got stale.
--publish with -gmanni: --publish writes graph.ttl into the index directory, so it cannot be combined with --graph. Use --index to choose the directory.
--no-authoring without --publishmanni: --no-authoring decides what --publish builds, so it needs --publish.

Builds the graph and writes everything serving needs into one directory. It runs each step’s own command in-process, in the order each step’s input exists.

Terminal window
manni graph publish [options]
Terminal window
$ manni graph publish --out docs/public/graph
build graph.ttl: 256 pages, 20577 triples
export search.en.json, localizations.json: 2920 entries
skipped embed: no graph.embed section
export graph.jsonld: 3003 nodes
export chunks.json, chunks.en.jsonl: 2359 chunks, 0 withheld
export llms.txt: 256 pages
skipped iirds: no graph.export.iirds section
Published 7 files to docs/public/graph/.
StepRuns whenWhat it writes
buildalwaysThe graph, under the file name config out gives it.
export:searchalwaysThe search index and its manifest.
embeda graph.embed section existsOne vector sidecar per language.
export:jsonldalwaysgraph.jsonld, which createRetriever reads.
export:chunksalwayschunks.json and its rows. See Chunks.
export:llmsa graph.export.llms section existsllms.txt. See llms.txt.
export:iirdsa graph.export.iirds section existsThe iiRDS package.

A step that is not set up prints a skipped line and never fails the run. Each step’s warnings and notices go to stderr as that step says them. The first step that fails stops the run with exit 2:

manni: graph publish stopped at export:llms: graph export llms needs graph.export.llms.origin, the site's scheme and host.

publish does not gate. Run graph check on what it published, so the cited row reads the new chunks: manni graph publish --out site/graph && manni graph check -g site/graph/graph.ttl.

With -f json it prints the steps:

{ "out": "docs/public/graph",
"steps": [ { "step": "build", "status": "ran", "detail": "graph.ttl: 256 pages, 20577 triples", "files": ["graph.ttl"] },
{ "step": "embed", "status": "skipped", "detail": "no graph.embed section", "files": [] } ] }

files names each file relative to out.

When the build holds strategy records, the build line counts them, as build graph.ttl: 344 pages, 23385 triples, 82 strategy records. The build step’s detail carries the same clause.

--no-authoring publishes a graph without author-facing content, and every step reads that build. A --no-authoring run refuses --evals with exit 2, because eval verdicts are author-facing:

manni: --evals attaches eval verdicts, which are author-facing, and this run leaves author-facing content out.

A configured graph.export.chunks.evals warns once instead, and the run exits 0:

manni: graph.export.chunks.evals is set, but eval verdicts are author-facing, so this run leaves them out.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-o, --out<dir>the directory of config outThe directory for every artifact.
--evals<path>config export.chunks.evalsA docevals JSON report for the chunks step.
--collection<name>every collectionA configured collection the build step reads. Repeatable, one name per occurrence.
--no-authoringn/aconfig authoring, else onLeave out author-facing content: strategy records, and every value a page’s schemas mark x-manni-location: external.
-f, --format<pretty|json>prettyOutput format.

Computes local embeddings for the search index, one sidecar per language. It needs the optional @huggingface/transformers peer; --embedding-model mock runs offline and is what regenerates the committed fixtures.

Terminal window
manni graph embed [options]
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file the index sits beside.
-i, --index<dir>beside the graphDirectory holding the indexes and the manifest.
-o, --out<dir>config embed.out, else the index directoryDirectory for the sidecars.
--embedding-model<id>config embed.modelEmbedding model id. Any id is accepted, because the documented table is the tested set rather than the permitted one. mock runs offline.
--dtype<dtype>config embed.dtype, else q8Weight quantization.
--no-cachen/aoffIgnore the vector cache.
-f, --format<pretty|json>prettyOutput format.

Reserializes the built graph into a consumer format. The target is a positional rather than a -f. Every other verb of the family means output format by -f, and one flag cannot mean two things.

Terminal window
manni graph export <target> [options]
Terminal window
$ manni graph export jsonld -c test/graph/fixtures/dd/docs.config.yaml
Wrote 32 nodes to .tmp/dd/graph.jsonld
TargetWhat it writes
jsonldJSON-LD, for a linked-data ingester.
iirdsA conformant iiRDS package, for a content delivery portal. RDF/XML is written inside it.
searchThe browser search index and its manifest, read client-side by @hawkeyexl/manni/graph/runtime.
questionsThe question set as JSON Lines, for an eval tool. See The question set.
chunksThe passages a retrieval host serves, each with its provenance record. See Chunks.
llmsllms.txt, one link and description per page. See llms.txt.

An unknown target is exit 2, with the allowed set named.

ArgumentDescription
<target>What to write: jsonld, iirds, search, questions, chunks, or llms. Required.
OptionArgumentDefaultDescription
-c, --config<path>discoveredPath to manni.config.yaml.
--no-confign/aoffIgnore any discovered config file.
-g, --graph<path>config outThe built Turtle file to read.
-o, --out<path>the graph path with the target’s extension; questions.jsonl beside the graph for questions; llms.txt beside the graph for llms; the graph’s directory for search and chunksWhere to write. For search and chunks it names a directory, because each writes more than one file.
--evals<path>config export.chunks.evalsA docevals JSON report, as manni docevals run -f json writes it. It fills each chunk record’s evals. Any target but chunks refuses it with exit 2.
--no-authoringn/aconfig authoring, else onLeave author-facing content out of what chunks and llms read from the pages. Any other target refuses it with exit 2.

chunks and llms read the pages as well as the graph, so --no-authoring shapes them. They skip strategy records and drop every value marked x-manni-location: external. The chunks carry no pins and no eval verdicts. jsonld, iirds, search and questions reserialize the graph as it was built, so they refuse the flag. A reader-only graph of those comes from a --no-authoring build or publish.

export questions writes one JSON line per question. It reads each page’s sample-questions and each file graph.questionFiles names. An answer’s text is rendered from its pin when the export runs, so the source stays pinned rather than copied.

Terminal window
$ manni graph export questions
Wrote 431 questions to graph/questions.jsonl
$ manni graph export questions -o eval/golden.jsonl -g build/graph.ttl -c ci/manni.config.yaml
Wrote 431 questions to eval/golden.jsonl
{"id": "docs/uploads/retry.md#how-many-times-does-an-upload-retry-by-default",
"question": "How many times does an upload retry by default?",
"language": "en", "reviewed": true, "answerable": true,
"expected": [{"id": "https://docs.example.com/graph/doc/docs/uploads/retry.md#retry-limits",
"file": "docs/uploads/retry.md", "lines": "41-43", "reviewed": true,
"text": "Uploads retry three times, waiting 2, 4 and 8 seconds."}]}

The file is shown wrapped here. On disk, each question is one line.

FieldMeaning
idThe page or golden-set file, then # and the question’s slug.
languageThe page’s language. A file question takes its own language, else its first answer’s page, else und.
reviewedfalse for a drafted question. Drafts are included for the consumer to filter.
answerablefalse for a gap, a file question with no answers. Its expected is empty.
expected[].idThe node export search indexes the answer’s first line under, so ID-based recall compares like with like.
expected[].fileThe page that holds the answer.
expected[].linesThe body lines the answer is on now. An answer that moved is written at its new lines.
expected[].reviewedfalse when the pin or its question is a draft.
expected[].staletrue when the answer changed since it was pinned, or its page is gone.
expected[].textThe answer’s lines, as the page reads at export time.

A question with no pin has one expected entry for its page, with no lines or text. A stale answer is written as its lines read now. A stale entry with no text is an answer whose page is gone. A pin whose text now matches in more than one place is written at its first match, and graph check reports the ambiguity. One warning on stderr gives the stale count, and the exit code is still 0:

manni: 5 answers changed since they were pinned, so they are exported as they read now, marked stale.

No question anywhere writes an empty file and reports Wrote 0 questions.

The format is manni’s own, because each eval tool names its fields differently. Each mapping below reads the reviewed questions from rows, the parsed lines.

# Ragas
samples = [{"user_input": r["question"], "reference_contexts": [a["text"] for a in r["expected"] if "text" in a]} for r in rows if r["reviewed"]]
dataset = EvaluationDataset.from_list(samples)
# DeepEval
goldens = [Golden(input=r["question"], context=[a["text"] for a in r["expected"] if "text" in a]) for r in rows if r["reviewed"]]
dataset = EvaluationDataset(goldens=goldens)
Terminal window
# promptfoo: point `tests: file://tests.jsonl` at the result
jq -c 'select(.reviewed) | {vars: {question: .question}, metadata: {id: .id}}' graph/questions.jsonl > tests.jsonl

export chunks writes the passages a retrieval host serves, and the proof behind each one. A passage is a section’s text, from its heading to the next heading of the same or higher rank. Each document’s whole body is a passage too. A passage is keyed by its graph node IRI.

Terminal window
$ manni graph export chunks --evals .manni/evals.json
Wrote 214 chunks to graph/chunks.json

It writes into one directory: -o names it, and the graph’s directory is the default.

FileWhat it holds
chunks.jsonThe manifest. It says what the rows were built from, and holds the retrieval defaults.
chunks.<tag>.jsonlOne JSON line per passage, one file per language.

Two exports of the same inputs are byte-identical. Keys are sorted, rows are sorted by id, and nothing reads the wall clock.

KeyMeaning
formatAlways "manni-graph-chunks".
versionAlways 1.
graphThe sha256- digest of the graph’s JSON-LD, the bytes graph export jsonld writes.
graphFileThat JSON-LD file’s name.
searchIndexThe search index’s localization manifest, as graph export search names it.
commitThe docs commit, or null outside git.
worktreeclean or modified, or null outside git.
retrieveThe resolved graph.retrieve keys, the retrieval defaults every host reads.
evals{ report, digest }: the report as the config or --evals names it, and its digest. null with no report.
languages{ "<tag>": { file, rows, model } }: each language’s rows file, how many rows it holds, and its model. model is { id, dimensions } of the embedding model that language’s vectors come from, or null with no vector sidecar. graph.embed.byLanguage can give each language its own.
pages{ "<page>": { metadata } }: each page’s delivered fields.
withheld[{ id, page, because }]: the passages graph.retrieve.withhold left out, and which reasons matched.
FieldMeaning
idThe passage’s graph node IRI.
graphThe manifest’s graph digest.
pageThe page the passage comes from.
linesThe passage’s body lines, N-M. A MadCap Flare topic’s span is its own source lines, even where a snippet supplies the text.
languageThe page’s language tag.
titleThe heading, or the page’s title for a whole body.
textThe passage, as the page reads at export time. A MadCap Flare topic’s passage is its text as its target builds it, with snippets expanded and variables resolved.
vectorThe node’s vector. Present only when a sidecar from manni graph embed holds one for that node.
metadataThe page’s delivered fields. The delivered fields of every heading meta statement from the page down to this section overlay them, the nearest winning field by field.
provenanceThe passage’s provenance record.

A record is self-contained. Copied out of its row, it still says what it describes.

FieldMeaning
node, page, linesWhat the record describes.
commit, worktreeAs in the manifest. Both are null outside git.
pinsThe cite pins whose claim overlaps the passage.
datesThe page’s derived and verified dates.
evalsThe page’s docevals verdicts, or null when no report is configured or the report holds none for the page.
statements[{ lines, status, data }]: the meta statements anchored inside the passage, with their delivered fields. A heading statement sits at its heading’s lines.

pins is { total, current, notice, warning, error, items }. Each pin is counted once, at its worst finding. The findings use cite’s own rules at the severity cite.severity sets, and they are computed outside git too. Each item is { id, claim, source, commit, findings, lastVerified, verifiedBy }. A finding is { rule, severity }, and an empty list means the pin is current.

dates holds created and lastUpdated, each { day, stamp }. day is the UTC day manni meta derive would stamp. It is null outside git, or for a body change not yet committed. stamp compares that day with the page’s own field: current, stale or absent. When day is null, so is stamp, since there is no day to compare. Beside them, lastVerified is the page’s last-verified as written, or null. verifiedBy holds its verified-by entries, or [].

evals is { report, current, pass, fail, needsReview, skipped, error, items }. report is the report’s digest. current says whether the page still has the bytes the report’s integrity names. Each item is { name, suite, grader, outcome }, plus confidence when the judge gave one.

A field reaches the manifest’s pages[].metadata, a row’s metadata and a statement’s data unless its schema marks it. The marks are x-manni-graph-output: false and x-manni-location: external, read at every depth. So a sample-questions item keeps its question and loses its answers pins, which manni:ai-context:1.2.0 marks. JSON-LD, iiRDS and the search index keep the graph build’s rule, which reads top-level fields only.

graph.retrieve.withhold leaves passages out of the rows and lists them under the manifest’s withheld. drifted matches a passage with a pin at error severity. failing matches a current eval verdict that failed or errored. A stale verdict never counts.

Each goes to stderr, prefixed manni: . Only the four marked exit 2 stop the export. The rest leave the exit code at 0.

MessageKind
no vector sidecar, so chunks carry no vectors and retrieval is lexical only; configure graph.embed to add them.warning
<N> chunks withheld by graph.retrieve.withhold (drifted); chunks.json lists them.notice
no docevals report is configured, so records carry no eval verdicts.warning
graph.export.chunks.evals names .manni/evals.json, which does not exist.exit 2
.manni/evals.json is not a docevals JSON report; write one with manni docevals run -f json.exit 2
.manni/evals.json records no page integrity, so every verdict reads stale; write it again with manni docevals run -f json.warning
4 pages changed since .manni/evals.json was written, so their verdicts read stale.warning
3 pages in .manni/evals.json are not in the graph, so their verdicts are left out.warning
git cannot read this directory (<git's reason>), so records carry no commit and no derived dates.notice, said once
2 pages differ from 26dad159, so their records describe uncommitted text.warning
--evals applies only to graph export chunks.exit 2
--no-authoring applies only to graph export chunks and llms; the others write the graph as built, so build it with --no-authoring.exit 2
--evals attaches eval verdicts, which are author-facing, and this run leaves author-facing content out.exit 2
graph.export.chunks.evals is set, but eval verdicts are author-facing, so this run leaves them out.warning, said once

Outside git the export still succeeds. Its records carry pins and evals, with no commit and no derived dates.

export llms writes llms.txt beside the graph, or to the file -o names. It needs graph.export.llms with a title and an origin.

# manni
> One CLI for docs-as-code.
## Docs
- [manni cite](https://hawkeyexl.github.io/manni/cite/): Pin a sentence to the source lines it rests on.

The H1 is title, and the blockquote is summary when it is set. A corpus in one language gets one ## Docs section. A corpus in several gets one section per language tag. Within a section, pages are in path order.

Each description is the page’s own description as delivered, so a field marked x-manni-graph-output: false stays out. Each link is origin plus the page’s route, from the nearest route whose root holds it. An index file names its directory. The route takes the URL style of the framework manni site detects.

FrameworkA page’s route
Starlight, MkDocs, Zensical, Hugo, or none detected/guide/setup/
Docusaurus, Nextra, Fumadocs, Mintlify, Fern/guide/setup
VitePress, Rspress, Sphinx, Jekyll/guide/setup.html
Flare/guide/setup.htm
MessageKind
graph export llms needs graph.export.llms.title.exit 2
graph export llms needs graph.export.llms.origin, the site's scheme and host.exit 2
2 pages in the graph are not on disk, so llms.txt leaves them out; run manni graph build.warning
2 pages are under no route, so llms.txt leaves them out.warning
CodeMeaning
0Everything passed. A warning or a notice is still 0.
1Findings. A SHACL error, a stats --check failure, or a fill error. The author’s problem.
2Operational or usage error. No input files matched, a bad config, an unknown flag value, a missing provider. Not the author’s problem.