Run it in CI
A pull request that breaks the docset’s structure should be red before review. Two commands do it, over the same collection the metadata gate already uses. There is no second tool to install and no second config to keep.
This is Devin’s page.
The job
Section titled “The job”name: Docs graph
on: pull_request: paths: - "docs/**" - "manni.config.yaml"
jobs: graph: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: # Git history is detected, not declared: creator, created and # modified come from commits. A shallow clone degrades those # silently, so fetch the lot. fetch-depth: 0 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npx @hawkeyexl/manni graph build - run: npx @hawkeyexl/manni graph check -f github - run: npx @hawkeyexl/manni graph stats --checkNothing in that job reaches a model or the network. build, check and
stats are deterministic and local, which is why the graph gate is cheap
enough to run on every push. fill is the one verb that reaches a provider,
and it is not a CI step.
The two steps do different jobs
Section titled “The two steps do different jobs”check validates the built graph against SHACL shapes. This is where the
emergent failures live. A concept split across two spellings. A broader
cycle closed by three pages. A variant a page is both about and excluded from.
They exist only after the corpus merges into shared nodes, so no per-file check
can see them. See Govern the graph.
stats --check gates two different things: a broken internal link, and
vocabulary coverage below its floor. See
Coverage.
Run both. Neither subsumes the other.
Annotations
Section titled “Annotations”-f github writes one workflow annotation per finding:
$ manni graph check -c test/graph/fixtures/dd/violations.config.yaml -f github::error file=violations/alpha.md::"Alpha" and "Beta" form a broader cycle, so each is its own ancestor. Remove one `broader` or `narrower` value on the cycle. (https://example.com/dd/concept/alpha)%0Amanni:graph-shapes:1.3.0 Concept-broader: A more general concept. A concept is never its own ancestor.%0AGuide: https://hawkeyexl.github.io/manni/graph/fix/The message ends with the focus node in parentheses. After each %0A, a line
names the shape behind the finding and says what it is for. The last line links
the fix page.
A graph finding has a focus node rather than a line, so the annotation lands on
the file rather than inside it. A finding can come from several pages, and the
annotation lands on the first page it names. The pretty report and the JSON
docs list name every one, because the fix may be in any of them.
check also accepts -f json, for a dashboard. Every other verb offers
pretty and json. github and sarif are check’s alone, because check
is the one that annotates a diff.
Code scanning
Section titled “Code scanning”-f sarif writes one SARIF 2.1.0 log for GitHub code scanning. A run with
findings is the one worth uploading, so the upload step runs if: always():
- run: npx @hawkeyexl/manni graph build - name: Graph check run: npx @hawkeyexl/manni graph check -f sarif > graph.sarif - uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: graph.sarif category: manni-graphEach result lists every page the finding names, at line 1. Its rule is the
shape’s, such as manni:graph/Concept-broader, with the shape’s description and
the fix page as its help. A finding that names no page has nowhere to land, so
it is left out of the results. The run’s notes count them instead:
2 findings name no page. Run manni graph check to see them. The exit code is
the same as in any other format, so an error still exits 1.
Exit codes
Section titled “Exit codes”The family’s contract, with nothing added:
| Code | Meaning | Whose problem |
|---|---|---|
0 | Clean. A warning or notice finding is still 0. | Nobody’s. |
1 | Findings. A SHACL error, a broken link or coverage below floor under stats --check, or a validate failure. | The author’s. |
2 | Operational. No input files matched, a bad config, or an unknown flag value. | Yours. |
A recipe that treats any non-zero exit as “the docs are bad” is wrong, and it routes your own failures to contributors. Split them:
- name: Graph check run: | npx @hawkeyexl/manni graph check -f github code=$? if [ $code -eq 2 ]; then echo "::error::manni graph could not run — this is a pipeline problem" fi exit $codeRamping in on a corpus that is not clean yet
Section titled “Ramping in on a corpus that is not clean yet”A first honest run on an unmeasured corpus reports a great deal. The instinct that follows is to weaken the rules until the build is green, and that destroys the standard for good. Two better moves:
Enter new shapes at sh:Warning. A finding at warning reports and exits
0. Promote the rule to sh:Violation when the corpus is clean. See
Severity.
Set coverage thresholds just below today’s numbers, and ratchet. Run
graph stats --check --coverage-threshold <n> to find the honest line, write the
per-field map, and raise it as the gaps close.
There is no findings baseline file for graph check, deliberately. A graph
finding’s identity is a focus node, and a focus node moves when a concept is
renamed. A recorded baseline would then forgive the rename as though it were
the same finding. Severity is the ratchet here instead.
A slow build
Section titled “A slow build”build reads every document and shells out to git. Two things keep it quick:
- Narrow the collection. A
graphgate over a docset rarely wants the changelog or the generated API dump in it.exclude:on the collection decides membership for every tool at once. - Trim
build.derive. A corpus with no images and no code blocks to model gains nothing from deriving them. Narrowing the list makes a smaller graph, not a wrong one.
Publishing on the same run
Section titled “Publishing on the same run”The artifact a retrieval system consumes should be the one that passed. Add the export to the job that already gated it:
- run: npx @hawkeyexl/manni graph export jsonld - run: npx @hawkeyexl/manni graph export search - uses: actions/upload-artifact@v7 with: name: graph path: graph/See Publish the graph.