Skip to content

manni strategy

Write down whom the docs serve, once, and hold every page to it.

A docset’s content strategy says whom it serves. manni strategy keeps that strategy as records, and checks every page that names one against them.

A record is a page that declares one of three types. It is one record per file, in any format manni meta reads.

  • An audience declares type: audience. It is a group of readers.
  • A persona declares type: persona, and names its audience in audience:.
  • A journey declares type: journey, and names the personas it serves in personas:. A journey can serve more than one persona.

A record’s id is its id:, or else its file name without the extension. list and get print its title and description. This is the journey this section serves, as this repository keeps it:

docs/content-strategy/journeys/s17.md
---
type: journey
id: s17
title: Hold the pages to the strategy they were written for
description: The audiences, personas and journeys Sara's docs serve are records, and every page that names one is checked against them.
personas: [sara]
---

The prose below the frontmatter is the journey itself. No rule reads it.

A journey can also carry lifecycle, from the manni:lifecycle:1.0.0 vocabulary. A draft journey is not live yet, and an archived one no longer is. Neither is expected to have a page, so check skips both when it looks for an unserved journey. A journey can land as a draft before its pages exist.

Records also carry the fields manni test tasks reads, where a synthetic user attempts a journey as one of its personas. No strategy rule reads them.

  • A journey’s trigger, entry-point and success-criteria tell the synthetic user what started the task, where to begin, and when it is done.
  • A journey’s workspace names the directory each run starts from.
  • A journey’s evals: are eval entries that say what done looks like. Journeys that declare evals: are the golden set, which a bare manni test tasks runs.
  • A persona’s goals, pains and constraints tell the synthetic user who it plays. Each constraint is judged after the run, as a warning.

manni meta validate checks a record’s fields against the schemas a repo applies through meta.overrides. This repository keeps one schema per record type, for an audience, a persona and a journey. Three overrides apply them. The journeys override also lists the lifecycle vocabulary, so a value such as lifecycle: drafty fails meta validate:

manni.config.yaml
meta:
overrides:
- files: "docs/content-strategy/audiences/*.md"
schemas: [./docs/content-strategy/schemas/audience.schema.json]
- files: "docs/content-strategy/personas/*.md"
schemas: [./docs/content-strategy/schemas/persona.schema.json]
- files: "docs/content-strategy/journeys/*.md"
schemas:
- ./docs/content-strategy/schemas/journey.schema.json
- manni:lifecycle:1.0.0

A page says whom it serves in three fields of the manni:audience:1.0.0 vocabulary. Each value is a reference, and the check resolves it against the records.

FieldNamesWhere it lives
audiences:audience recordsthe page
personas:persona recordsthe page or its {page}.meta.yaml sidecar
journeys:journey recordsthe page or its {page}.meta.yaml sidecar

A value in a sidecar counts as one in the page does. A finding about it sits at the sidecar’s line.

A field is checked only when the set holds a record of its type. A docset that keeps persona records and no audience records keeps a free-form audiences:, and no rule reads it. So the records can arrive one type at a time.

The records and the pages come from the family’s collections. Give the records a collection of their own. This is this repository’s config, cut to the lines the domain reads:

manni.config.yaml
collections:
- name: site
paths:
- "docs/src/content/docs/**/*.{md,mdx}"
externalMetadata:
- file: "{page}.meta.yaml"
- name: strategy
paths:
- "docs/content-strategy/{audiences,personas,journeys}/*.md"

The overrides above then check each record’s fields.

A record can also rest on evidence. Each goal or pain is pinned to the interview line it came from, and manni cite check says when one stops holding. Rest personas on evidence sets that up.

The strategy: section is optional, because every key has a default. The configuration reference lists the keys. This repository raises one rule, so a journey cannot land without a page that serves it, unless it lands as a draft:

manni.config.yaml
strategy:
severity:
unserved-journey: error
CommandDoes
manni strategy list [paths...]Prints the records as a tree, with how many pages name each one.
manni strategy get <id> [paths...]Shows one record, its parents, the records under it, and the pages that name it.
manni strategy check [paths...]Checks every reference against the records, and the records against each other.

Run with no paths, each command reads every collection. Here they run over a set of one audience, one persona and two journeys, which two pages name. list prints the tree:

Terminal window
$ manni strategy list
record pages title
docs-engineers 2 Docs-as-code teams
maya 2 Maya, Documentation Engineer
m1 2 Stand up metadata validation for my repo
m2 1 Tighten the standard without breaking the build
1 audience, 1 persona, 2 journeys

get matches the id in any case. Its pages row answers which pages serve the record:

Terminal window
$ manni strategy get maya
Maya, Documentation Engineer strategy/personas/maya.md:1
type persona
id maya
description Owns a docs-as-code repo and wants its frontmatter correct.
audience docs-engineers Docs-as-code teams
journeys m1 Stand up metadata validation for my repo
m2 Tighten the standard without breaking the build
pages docs/ci.md
docs/guide.md

Every reference resolves, so check prints one line and exits 0:

Terminal window
$ manni strategy check
✓ 4 records, 10 references, no findings

In another set, a guide names a journey that has no record, and a journey whose persona the page leaves out:

docs/guide.md
---
title: Guide
audiences: [docs-engineers]
personas: [devin]
journeys: [m1, m9]
---

check reports both at the line of the value that caused them:

Terminal window
$ manni strategy check
docs/guide.md:5
warning manni:strategy/missing-parent personas: does not name "maya", the persona of journey "m1".
Add it, or drop "m1" from journeys:.
manni:strategy missing-parent: A page names a journey but none of its personas in personas:, or a persona but not its audience in audiences:.
Guide: https://hawkeyexl.github.io/manni/strategy/reference/rules/#missing-parent
error manni:strategy/undefined-reference journeys: "m9" names no journey.
Did you mean "m1"?
Correct the value, or add a record with type: journey and id: m9.
manni:strategy undefined-reference: A page's audiences:, personas: or journeys:, or a record's audience: or personas:, names no record of that type.
Guide: https://hawkeyexl.github.io/manni/strategy/reference/rules/#undefined-reference

That set breaks every rule once, so the run goes on to five findings about the records. It ends 3 errors, 3 warnings, 1 notice in 7 records, and it exits 1 because of the errors. Warnings and notices never fail a run.

A journey’s steps: lists its stages in order, each with the page that carries it. A step’s doc is a repo path, or a site route that graph.routes maps to a file. A page still to be written says exists: false:

strategy/journeys/m1.md
---
type: journey
id: m1
title: Stand up metadata validation for my repo
personas: [maya]
steps:
- stage: Read the guide
doc: docs/guide.md
- stage: Add the CI step
doc: /site/ci/
- stage: Look up a flag
doc: docs/reference.md
- stage: Work through the tutorial
doc: /site/tutorial/
exists: false
---

get then shows the path in order, with each step’s state and page:

Terminal window
$ manni strategy get m1
Stand up metadata validation for my repo strategy/journeys/m1.md:1
type journey
id m1
personas maya Maya, Documentation Engineer
steps 2 of 4 served
1 served Read the guide
docs/guide.md
2 served Add the CI step
docs/ci/index.md
3 unclaimed Look up a flag
docs/reference.md
4 unwritten Work through the tutorial
/site/tutorial/
pages docs/ci/index.md
docs/guide.md

The reference page carries stage 3, but its own journeys: leaves out m1. check names that as a missing-journey warning on the step’s doc: line. It names the unwritten tutorial as an unwritten-step notice. A doc that names no page at all is a broken-step error. list adds a steps column with each journey’s served steps over its steps. The steps section of the rules reference has every state.

Several docsets can serve the same readers from one set of records. Keep the records in one repository, and check it out beside each repository that reads them:

work/
manni/ owns docs/content-strategy/
manni-plugin/ borrows it
manni.config.yaml
docs/
strategy/journeys/ the journeys only the plugin serves

In the borrowing repository, the strategy collection names the owner’s folders as directories. A glob that matches nothing is not an error, so a missing checkout would quietly narrow the run. A missing directory stops it with exit 2:

manni-plugin/manni.config.yaml
collections:
- name: site
paths: ["docs/**/*.md"]
- name: strategy
paths:
- ../manni/docs/content-strategy/audiences/
- ../manni/docs/content-strategy/personas/
- ../manni/docs/content-strategy/journeys/
- strategy/journeys/

A record or page read from outside the run’s directory is borrowed. The run’s directory is the config file’s, or the working directory when no config file governs the run. A checkout inside that directory reads as the repository’s own, so check out side by side.

A borrowed record resolves every page’s references, as an owned one does. check files no finding in a borrowed file. The owner’s own check reports those. The summary counts the borrowed records:

Terminal window
$ manni strategy check
✓ 84 records (83 borrowed), 90 references, no findings

A kept copy of a borrowed record draws duplicate-id, and its fix says to remove the copy:

Terminal window
$ manni strategy check
strategy/personas/maya.md:3
error manni:strategy/duplicate-id id: "maya" is also the id of ../manni/docs/content-strategy/personas/maya.md:3.
That record is borrowed from outside this repository.
Remove this copy, or give it its own id.
manni:strategy duplicate-id: Two records share an id, ignoring case.
Guide: https://hawkeyexl.github.io/manni/strategy/reference/rules/#duplicate-id
1 error in 85 records (83 borrowed)

The journeys the borrowing repository owns are checked as before. One that no page names still draws unserved-journey. A borrowed journey’s steps name its owner’s pages, so get shows each as unchecked, with no coverage line.

In CI, check out both repositories side by side. The second checkout’s ref: pins the records to a tag of the owner’s:

.github/workflows/docs.yml
- uses: actions/checkout@v7
with:
path: manni-plugin
- uses: actions/checkout@v7
with:
repository: your-org/manni
path: manni
ref: strategy-v3
sparse-checkout: docs/content-strategy
- run: npx @hawkeyexl/manni strategy check -f github
working-directory: manni-plugin

A typo on a plugin page annotates that page, and nothing annotates ../manni/.

Coverage across products is data. Each repository’s strategy list -f json prints every record with the pages that name it. Joined by id, the files give pages per journey per repository:

Terminal window
$ (cd manni && manni strategy list -f json > ../manni.json)
$ (cd manni-plugin && manni strategy list -f json > ../manni-plugin.json)
$ jq -nc '[inputs | (input_filename | split("/") | last | rtrimstr(".json")) as $repo
| .records[] | select(.type == "journey")
| {id, repo: $repo, pages: (.namedBy.pages | length)}]
| group_by(.id)
| map({id: .[0].id, pages: (map({(.repo): .pages}) | add)}) | .[]' manni.json manni-plugin.json
…
{"id":"d16","pages":{"manni":3,"manni-plugin":1}}
…
{"id":"m1","pages":{"manni":10,"manni-plugin":1}}
…
{"id":"p1","pages":{"manni-plugin":1}}
…

Each repository is its input file’s base name. Append | select([.pages[]] | add == 0) to list the journeys no product serves. The ids match on both sides because there is one set of records. A journey one product owns can share an id with another product’s, so give owned journeys a product prefix.

The check says the pages agree with the records. It cannot say whether readers take the journeys. Measure the strategy against reader traffic joins a page-views export to the records, for traffic per journey and per persona.

Each finding names its rule, and its Guide: line links to that rule’s section of the rules reference. That page gives each of the ten rules its default severity, its exact message, and the edit that clears it. It also has the full run of the set above.

What manni strategy checks, and what it doesn’t

Section titled “What manni strategy checks, and what it doesn’t”

It checks

That every audiences:, personas: and journeys: value names a record of its type. That a record’s audience: or personas: names one too. That no two records share an id, ignoring case. That a page naming a journey also names one of its personas, and a persona its audience. That some page names every journey that is not a draft or archived. That each journey step names a page, and that the page names the journey.

It doesn't

Read the prose of a record or a page. It doesn’t require a page to carry journeys: either. That is a question of presence, so it belongs to manni meta, as a schema override with required: [journeys].

manni graph build types every record in its build. An audience becomes a schema:Audience, a persona a schema:PeopleAudience, and a journey a graph:Journey. Each node sits at {baseIri}strategy/{type}/{id}. A page’s journeys: become dcterms:isPartOf edges, and its audiences: and personas: become schema:audience edges. So the graph answers which pages serve a journey:

Terminal window
$ manni graph build -o .manni/graph/internal/graph.ttl
Wrote .manni/graph/internal/graph.ttl (344 docs, 23395 triples, 560 concepts (50 from the termbase), 82 strategy records)
$ manni graph traverse -g .manni/graph/internal/graph.ttl https://hawkeyexl.github.io/manni/id/strategy/journey/s17 --reverse --predicates dcterms:isPartOf
https://hawkeyexl.github.io/manni/id/strategy/journey/s17 — Hold the pages to the strategy they were written for
https://hawkeyexl.github.io/manni/id/doc/docs/src/content/docs/strategy/index.mdx — manni strategy
https://hawkeyexl.github.io/manni/id/doc/docs/src/content/docs/strategy/reference/cli.mdx — CLI reference
https://hawkeyexl.github.io/manni/id/doc/docs/src/content/docs/strategy/reference/rules.mdx — Check rules
4 nodes, 3 hops, 0 excluded

An edge needs the record it names in the build. A value no record declares derives nothing, and check reports it here as undefined-reference.

Records are author-facing, so graph build --no-authoring leaves them out. This repository’s site publishes only its site collection, which holds no record. How manni graph works lists every triple a record derives.

The domain is in play once a collection holds a record. manni status then shows the strategy row, with a count of each type. manni check runs manni strategy check beside the other domains’ checks. Gate every check covers the family verbs, and the family CLI reference has the in-play rule for each domain.

manni cite check reads the same records. A citation finding on a page that names a persona or journey record says whom the page serves. Its pretty report ends with a roll-up of those records, worst first, and its json carries them as impact. A value with no record is left out, since manni strategy check already reports it. Who a finding affects shows the output.

manni check prints the same roll-up over every check’s findings, so a run that fails several domains is ranked by the readers it reaches. Fix first what your readers hit walks through it.

manni meta get names the records a page names, one record line each, with the record’s type, id and file. The Claude Code plugin hands an agent the same references when it reads the page, as Keep an agent’s docs green shows.

A judged eval with readers: personas reads the descriptive fields of each persona its page names. See Judge a page as its readers.