Convention: covers: / owns: frontmatter
Every page under docs/app/business-logic/** declares which code paths
it documents via YAML frontmatter at the top of page.mdx. The
docs-update skill
and the CI safety check both consume this declaration. Without it,
the skill can’t know which doc to propose edits for when code changes,
and the CI check has no rules to enforce.
This page is the canonical reference. The full implementation lives in spec 003 .
The two fields
---
title: "Business logic: Suspend & unsuspend skills"
description: "..."
# Wide net (drift-detection): paths that, if changed, *might* require a
# doc update. Globs are repo-root-relative. Brace expansion supported.
covers:
- api/src/features/account/change-request/**
- api/src/features/account/logbook/**
# Narrow net (ownership claim): paths the page *directly defines* the
# behaviour of. Every owns: glob MUST also be matched by covers:
# (lint-enforced).
owns:
- api/src/features/account/change-request/**
---covers: — wide net
The set of code paths the page documents or refers to. When any of these paths change, the docs-update skill flags the page as potentially affected and offers to propose an edit (interactive yes/no).
Think of it as the answer to: “Which code paths, if changed, might need a corresponding update to this doc?”
owns: — narrow net (optional)
A strict subset of covers:. The set of paths the page directly
defines the behaviour of. When any of these change, the skill
auto-proposes an edit without prompting.
Think of it as the answer to: “Which code paths does this doc claim to be the source of truth for?”
Every entry in owns: MUST also be matched by at least one entry in
covers: on the same page. Enforced by npm run docs:lint-covers.
coversRationale: — opt-out (when covers is empty)
A page may opt out of covers: by setting it to an empty array and
adding a coversRationale: explaining why no code path applies:
---
title: "Business logic: Approval levels"
description: "..."
covers: []
coversRationale: >
Approval levels are a conceptual layer documented here for reference;
the rules are implemented across many features rather than at a
single code path. See specific feature pages (suspend-unsuspend-skills,
currency, etc.) for the actual code paths.
---The lint accepts these; the docs-update skill skips them.
The five lint rules
Run npm run docs:lint-covers from docs/. Five rules, all enforced
on every PR touching docs/**:
| Rule | Pass condition |
|---|---|
covers-resolves | Every covers: glob matches at least one tracked file in the monorepo. |
owns-resolves | Every owns: glob matches at least one tracked file. |
owns-subset-of-covers | Every owns: glob is also matched by at least one covers: glob on the same page. |
opt-out-rationale-required | If covers: [], then coversRationale: MUST be non-empty. |
malformed-frontmatter | The YAML frontmatter parses cleanly. |
When to add owns:
Add owns: only when the page genuinely defines the behaviour of a
code path. Sometimes the page just describes code that’s defined
elsewhere — in that case, covers: is enough.
Heuristic
Ask yourself: “If a developer rewrote this code path tomorrow, should the answer to ‘what does it do?’ change in this doc?”
- Yes → add it to
owns:. The doc is the source of truth. - No, but the doc mentions it → keep it in
covers:only. The doc references the behaviour but isn’t authoritative. - No, and the doc doesn’t really touch it → don’t add it. Drift detection on irrelevant paths just creates noise.
Examples
A page that owns a single feature
covers:
- api/src/features/account/change-request/**
- api/src/features/account/logbook/**
owns:
- api/src/features/account/change-request/**The page is the source of truth for change-request/**. It mentions
logbook/** because the suspend flow updates the logbook, but the
logbook’s own behaviour is documented elsewhere.
A page that owns nothing (cross-cutting policy)
covers: []
coversRationale: >
Cross-cutting policy doc; behaviour is implemented across many
features.Approval levels, currency cycles, conventions — these don’t map to a single code path. The opt-out is the right call.
A page that covers a shared helper
covers:
- api/src/shared/billing/**
# No owns: — this page mentions billing helpers but doesn't define them.docs-update will prompt before proposing edits, because the page
might or might not need updating depending on what changed.
How the skill uses this
When you run /docs-update (or npm run docs:update), the skill:
- Computes your branch diff vs
main. - Walks every
/business-logic/page and parses its frontmatter. - For each changed file, classifies into one of four buckets:
owned→ auto-propose an edit (no prompt).covered→ prompt: “page X covers this path but doesn’t claim ownership. Propose an edit? [y/N]”.unmatched(matchesbusinessLogicRootsbut no page covers it) → flag as a coverage gap.irrelevant→ do nothing.
The skill never writes to owns:-matched pages without an LLM
reasoning step about whether the change is genuinely behavioural. See
spec 003 § 5
for the full classification → action mapping.
How the CI safety check uses this
The blocking docs-required workflow doesn’t read covers: per se —
it consults the configured businessLogicRoots globs from
docs/.docs-update.config.json. If your PR touches those paths but
no docs/** files, the check fails (unless the PR description
contains docs-update: not-needed (reason: ...)).
The covers: map is consulted by the skill itself, not by the CI
gate. The two layers are complementary:
covers:/owns:= “which doc page is on the hook for this change?” → drives skill proposals.businessLogicRoots= “is this a business-logic change at all?” → drives the deterministic gate.
See also
- docs-automation spec (003)
- Naming convention — sibling convention page
docs-updateskill source