Govern the graph
Your standard about structure probably lives in review comments. A concept
should have one label. A page should belong to a section. A how-to should say
which product variant it is about. manni graph check is where those become
checks, enforced across the corpus rather than page by page.
This is Sara’s page. The outcome is that your house rules hold in the graph, and that a delivered artifact carries what you decided it should.
Why the graph, and not another per-file lint
Section titled “Why the graph, and not another per-file lint”A per-file check sees one file. The failures that matter most here exist only after N documents merge into shared nodes:
- Two pages spell one concept differently, so one node ends up with two preferred labels.
- Three pages’
broaderlinks close a cycle, so a concept is its own ancestor. - A page declares a product variant it is also excluded from.
- A link resolves to a page that no longer exists.
None of those is visible in any single document. That is what check is for,
and why it runs over the built graph rather than over the sources.
The bundled shapes are the floor
Section titled “The bundled shapes are the floor”manni graph check ships with a shapes file, shapes/graph/shapes-1.4.0.ttl, which
is what it uses when you name none. It declares a node shape per class the
emitter mints: graph:Document, graph:Section, graph:Block,
graph:Verification, skos:Concept, skos:ConceptScheme,
iirds:ProductVariant, and the PROV agents, activities, attributions and
associations. The strategy’s records get schema:Audience,
schema:PeopleAudience and graph:Journey. A block’s lines, pin and pin status, and a verification’s tool
and dates, are checked there. It adds three SKOS integrity checks core SHACL
cannot express: skos:broader cycles, skos:related conflicting with
skos:broaderTransitive, and a mapping such as skos:exactMatch that points
into the scheme itself.
Every one of them is closed. A predicate the shapes have not learned fails,
the way additionalProperties: false fails an unknown key. That is the feature
rather than a rough edge. A change to what the graph contains is not finished
until the shapes say so, in the same commit.
A published shapes file is immutable. Evolving them means a new three-segment version file beside the old one. MAJOR means a graph that used to pass now fails. MINOR means one that used to fail may now pass. PATCH means no change in validation behavior.
Adding your own
Section titled “Adding your own”Write a shapes file and point graph.check.shapes at it:
graph: check: shapes: - node_modules/@hawkeyexl/manni/shapes/graph/shapes-1.4.0.ttl - shapes/house-rules.ttlNaming shapes replaces the bundled set rather than adding to it. List the bundled file first when you mean to extend it, as above. Leave it out and you are running your rules alone, which is occasionally what you want and never what you meant by accident.
--shapes <path> does the same for one run. It is repeatable, one path per
occurrence, and never split on commas, because a path may hold one.
A house rule looks like any other property shape. This one says every concept must sit in a scheme:
@prefix sh: <http://www.w3.org/ns/shacl#> .@prefix skos: <http://www.w3.org/2004/02/skos/core#> .@prefix house: <https://example.com/shapes#> .
house:ConceptInScheme a sh:NodeShape ; sh:targetClass skos:Concept ; sh:property [ sh:path skos:inScheme ; sh:minCount 1 ; sh:severity sh:Warning ; sh:message "Concept belongs to no scheme. Give it one, or drop it." ; ] .Two things to get right. Target a class the emitter actually mints, or the
shape matches nothing and passes silently. And do not declare your own shape
sh:closed over a class the bundled shapes already close. The two closures
would each reject what the other allows.
Speak to the author
Section titled “Speak to the author”A finding is read by whoever wrote the page, so a house rule can say what to change in their words. Four parts carry that:
| Part | Where | What the finding shows |
|---|---|---|
sh:message | A shape | The message. Name the frontmatter key and say what to change. |
sh:description | A shape | What the rule is for, on the line under the message. |
sh:name | A property shape | The frontmatter key that feeds the property. |
owl:Ontology header | The file | The id from dcterms:identifier, and the guide link from rdfs:seeAlso. |
<https://example.com/house/shapes> a owl:Ontology ; dcterms:identifier "house:shapes:1.0.0" ; rdfs:seeAlso <https://example.com/house/guide/> .A file with no header is named by its path, as given, and its findings carry no
guide link. manni graph shapes lists every shape in force with its file’s id,
and manni graph shapes show <shape> prints each property with its key and
description. See graph shapes.
Severity, in the family’s words
Section titled “Severity, in the family’s words”A shape’s sh:severity maps onto the family scale, and the source value stays
in shaclSeverity for anyone who needs it. The mapping is the same one a11y
uses for axe’s impact.
sh:severity | Family severity | Effect |
|---|---|---|
sh:Violation | error | Exits 1. |
sh:Warning | warning | Reported; exit stays 0. |
sh:Info | notice | Reported; exit stays 0. |
A property shape that names no severity gets SHACL’s default, sh:Violation,
and therefore error. That is deliberate and worth knowing: a rule you meant
as advisory blocks the build until you say sh:Warning.
This is how a new rule lands without a wall of red. Enter it at sh:Warning,
watch what it catches for a sprint, then promote it to sh:Violation when the
corpus is clean. The alternative is to land it at error and then weaken the
rule to get green, which destroys the standard for good.
What the published graph carries
Section titled “What the published graph carries”Which fields reach an exported artifact is the schema’s call, not this tool’s. Mark a top-level property in the schema that governs the page:
{ "properties": { "owner": { "type": "string", "x-manni-location": "external", "x-manni-graph-output": false } }}Absent means true. The filter runs once, before derivation. A dropped
field is therefore absent from the Turtle, the JSON-LD, the iiRDS package and
the search index alike. There is no post-processing step to forget.
The marks come from the schema set manni meta validate resolves for each
page, so the meta: section of manni.config.yaml decides them. graph keeps
no schema set of its own. The default set carries manni:stewardship:1.2.0,
which marks owner, stakeholders and reviewed-by. Those name people, so
a published graph leaves them out with no config line.
The mark sits beside
x-manni-location in the same file on
purpose. What a page stores and what a delivered artifact says are two
decisions. They belong in one place, made by the person who owns the
standard.
Where a field is stored does not change its mark. graph reads each page with
every field its external-metadata manifest holds, as manni meta validate
does. An owner kept in a {page}.meta.yaml is left out like one on the page.
A mark nested inside the graph block is ignored. The mark governs a top-level
key, and graph is the top-level key.
One schema that marks a key both true and false on paths every page takes
is refused when it compiles, exit 2:
steward.schema.json: "x-manni-graph-output" says both true and false for "owner".Those paths are the schema’s own properties, its allOf entries, and a local
$ref reached from them. Marks that differ across anyOf, oneOf or
if/then branches are not an error. When the validator evaluates both, that
schema has no preference for the key, and an earlier schema’s mark applies.
Sample questions find their pages
Section titled “Sample questions find their pages”A page’s sample-questions are the questions a reader asks to reach it.
graph build carries each into the graph as graph:sampleQuestion, in the
page’s language. graph check asks each one through the retrieval
graph search uses, against an index it builds from the graph. A page outside
the top results for its own question is a finding.
graph: check: questions: top: 5 severity: warningtop is how high the page must rank. severity is the finding’s level, and
off asks nothing. Start at warning, as with a new shape, and promote to
error once the corpus is clean. The check measures whether a reader finds the
page. Nothing in it judges whether the page answers, so the result is the same
on every run. Fix the graph
covers a miss.
Three pointers a machine may never claim
Section titled “Three pointers a machine may never claim”/graph/sections, /graph/revision-of and /graph/derived-from are hand-curated. A
meta-provenance entry naming one of them is a graph check finding at error.
The guard lives here rather than in the schema. meta-provenance pointers
became free-form, and a schema cannot express “this pointer, but not from a
machine”. What it protects is the part of the graph a reader may assume a
person decided. Which sections a page declares, what it supersedes, and what it
was derived from.
Encrypted values
Section titled “Encrypted values”graph never decrypts. A value encrypted with the family key is harvested like any
other and lands in the graph as its ~… token. The token says a value exists
and nothing about what it is. A shape can therefore require the field’s
presence without the graph publishing its content.
Related
Section titled “Related”- Coverage, the other half of governing a corpus: what share of it carries each field, and how to gate on that.
- Run it in CI, where
checkbecomes a gate. - Configuration reference, for the
check.shapeskey.