Headings and scopes
Require, allow, deny, and count headings at each level of a document. Rules bind in document order by default, and strict scopes reject headings that no rule matches.
Markdown outline validator
Outlint validates headings and YAML frontmatter against a declarative schema. Use the command-line tool in local checks and CI, or embed the IO-free Rust library in your own tooling.
Current release: Outlint 0.1.0 is the first public release. See the installation commands, the changelog, and the pre-built binaries on GitHub Releases.
version: 1
title: "*"
sections:
- match: "Overview"
required: true
- match: "Decision"
required: true
# Move audit logs to object storage
## Overview
Store immutable audit logs in the archive bucket.
$ outlint check adr.md --schema .outlint.yml
adr.md:1:1 [missing-section] matched 0 sections, but at least 1 are required
expected: "Decision"
rule: .outlint.yml:6:5
1 diagnostic in 1 fileOutlint parses each Markdown document into a section tree and checks that tree against the schema. It reports diagnostics; it never rewrites the document.
Require, allow, deny, and count headings at each level of a document. Rules bind in document order by default, and strict scopes reject headings that no rule matches.
Match exact text, globs, regular expressions, or any heading. Express ordering, dependencies, conflicts, and groups between named rules.
Require or forbid frontmatter, validate it with an inline or linked JSON Schema, and use frontmatter values in outline constraints.
Diagnostics have stable IDs, document locations, and structural schema locations. The CLI also provides complete JSON output.
Constraints express relationships that a section tree alone cannot: alternatives, dependencies, exclusions, and orders that the rule list does not already impose. They can connect headings to each other and to YAML frontmatter.
References are scoped. A bare ref such as summary names a direct child rule where the constraint is attached. Use a dotted path such as deployment.rollback-plan to descend, or $.overview to anchor at the schema root. Exact matchers receive implicit IDs; pattern matchers need an explicit id. A constraint attached to a rule is evaluated separately for each instance of that parent.
A heading ref is true when at least one complete matching path exists. Put structural requirements such as “every API section has Errors” on the nested rule with required: true; constraints test presence, not every repeated instance.
one_oftwo or more refs
Exactly one listed proposition must be true. Use it when the document must choose one alternative.
one_of: [changelog, release-notes]any_oftwo or more refs
At least one listed proposition must be true; several may be true.
any_of: [summary, abstract, overview]at_most_onetwo or more refs
Zero or one listed proposition may be true. This permits no selection but rejects overlapping alternatives.
at_most_one: [stable-api, experimental-api]all_or_nonetwo or more refs
Either every listed proposition is true or none is. Use it for sections that form one optional unit.
all_or_none: [request, response, examples]requiresone if; one or more then
When if is true, every then ref must be true. A single consequence may be a scalar; write a list when several are required.
requires: { if: deployment, then: [rollback-plan, monitoring] }conflictsone if; one or more then_not
When if is true, every then_not ref must be false. Use it for sections or frontmatter states that cannot coexist.
conflicts: { if: deprecated, then_not: roadmap }orderedtwo or more heading refs
For the listed refs that are present, every occurrence of each must precede every occurrence of the next. Unlisted siblings may appear between them. Every scope is ordered by its rule list by default, so the constraint is refused there (ordered-scope-mismatch); use it in a scope declared ordered: false (options.ordered_sections: false for the top-level scope) to spell a partial order, or an order that differs from matching precedence. Refs must share one concrete scope; fm. refs and paths through repeated ancestors are not orderable.
ordered: [overview, design, examples]Use these anywhere a constraint accepts a ref, except in ordered. They address the document from any constraint scope.
fm.statusstatus exists and is not null. Dotted keys descend through mappings: fm.release.channel.fm.status=deprecatedmatch_case. An =null proposition is therefore never true.requires: { if: fm.status=deprecated, then: migration }
requires: { if: breaking-changes, then: fm.semver=major }Frontmatter refs deliberately stop at presence and typed scalar equality. Put patterns, comparisons, type checks, and keys containing . or = in the JSON Schema.
Rules use first-match-wins semantics within a heading scope, and their list order is the document order by default. Cardinality is checked per parent, and constraints refer to rules by ID. The specification is the normative reference.
---
status: published
---
# Storage guide
## Overview
How the storage service is organised.
## API: Upload
### Parameters
`bucket`, `key`, and `body`.
### Examples
See the client repository.
## Appendix A
Glossary.
---
status: deprecated
---
# Legacy storage API
## Overview
This API is retired.
## API: Upload
### Parameters
`bucket` and `key`.
version: 1
options:
match_case: false
frontmatter:
required: true
schema: # inline JSON Schema, or a path to a .json file
type: object
properties:
status:
enum: [draft, published, deprecated]
required: [status]
title: "*"
sections:
- match: "Overview"
required: true
- id: api
match: "/API: .+/" # anchored regular expression
repeat: "0..n"
strict: true
sections:
- match: "Parameters"
required: true
- match: "Examples"
required: false
- match: "Migration"
required: false
- match: "Appendix *" # glob
constraints:
- requires:
if: fm.status=deprecated
then: migration
$ outlint schema check .outlint.yml
(no output; exit status 0)$ outlint check docs/guide.md
(no output; exit status 0)$ outlint check docs/legacy.md
docs/legacy.md:1:1 [requires] a required consequence is missing
references:
- fm.status="deprecated"
- migration (exact "Migration")
constraint: .outlint.yml:36:5
1 diagnostic in 1 fileExact matchers receive generated IDs, so the constraint can refer to migration without an explicit id field; pattern matchers such as the regular-expression rule receive none and need one, here id: api, to be referenced. No ordered constraint is needed: Overview must precede the API sections because the rule list is the document order. Use strict: true to reject unmatched children in a scope. docs/guide.md passes; docs/legacy.md is deprecated without a Migration section, so the requires constraint reports it.
outlint check discovers the nearest .outlint.yml separately for each Markdown file unless you pass --schema. Human output is quiet when validation succeeds; use --format json in scripts and integrations.
cargo install outlintnpm install --global @outlint/cliversion: 1
title: "*"
sections:
- match: "Context"
required: true
- match: "Decision"
required: true
- match: "Consequences"
required: true
# ADR 0042: Retire the legacy upload API
## Context
The legacy endpoint has no remaining callers.
## Consequences
Clients must migrate to the v2 upload API.
## Decision
Remove the endpoint in the next major release.
$ outlint check docs/adr-0042.md
docs/adr-0042.md:1:1 [ordered] sections are out of the declared order: `Decision` must precede `Consequences`
observed order:
docs/adr-0042.md:7:1 "ADR 0042: Retire the legacy upload API > Consequences"
docs/adr-0042.md:11:1 "ADR 0042: Retire the legacy upload API > Decision"
schema: .outlint.yml:2:8
1 diagnostic in 1 file$ outlint check docs/adr-0042.md --format json{
"results": [
{
"diagnostics": [
{
"id": "ordered",
"involved_headers": [
{
"header_path": [
"ADR 0042: Retire the legacy upload API",
"Consequences"
],
"location": {
"column": 1,
"line": 7
}
},
{
"header_path": [
"ADR 0042: Retire the legacy upload API",
"Decision"
],
"location": {
"column": 1,
"line": 11
}
}
],
"location": {
"column": 1,
"line": 1
},
"message": "sections are out of the declared order: `Decision` must precede `Consequences`",
"schema_location": {
"column": 8,
"line": 2,
"path": ".outlint.yml"
},
"schema_node": {
"kind": "title"
},
"target": {
"kind": "document"
}
}
],
"kind": "document",
"path": "docs/adr-0042.md",
"schema": ".outlint.yml"
}
],
"summary": {
"diagnostics": 1,
"documents": 1,
"files": 1,
"schemas": 0
},
"version": 2
}{"results":[{"diagnostics":[{"id":"ordered","involved_headers":[{"header_path":["ADR 0042: Retire the legacy upload API","Consequences"],"location":{"column":1,"line":7}},{"header_path":["ADR 0042: Retire the legacy upload API","Decision"],"location":{"column":1,"line":11}}],"location":{"column":1,"line":1},"message":"sections are out of the declared order: `Decision` must precede `Consequences`","schema_location":{"column":8,"line":2,"path":".outlint.yml"},"schema_node":{"kind":"title"},"target":{"kind":"document"}}],"kind":"document","path":"docs/adr-0042.md","schema":".outlint.yml"}],"summary":{"diagnostics":1,"documents":1,"files":1,"schemas":0},"version":2}Exit status is 0 for valid inputs, 1 for validation diagnostics, and 2 for usage or operational errors. Every example on this page is a real file under examples/ in the site repository; the output shown is captured from the outlint build named in the terminal heading.
outlint-core is the pure, IO-free library behind the command-line tool. It never reads files, touches the network, or sets an exit status: callers provide schema and Markdown source text and receive normalized data and diagnostics.
load_schema turns schema text into a checked schema; PreparedValidator compiles it once and validates any number of parsed documents. Schema errors are collected together, never returned as a partial schema.
Each diagnostic has a stable ID, a document location, a typed target that distinguishes a heading that exists from one the schema expected, and a schema-node address that resolves back to the schema source.
A schema whose frontmatter.schema names a JSON Schema file is loaded through load_schema_with_resources: the library lists the files to read and the caller supplies their contents, so validation is reproducible and never fetches remote references.
Usage examples, the loading and validation boundary, and every public type are documented on docs.rs. The specification defines the semantics the library implements.
Outlint is currently a 0.x project. Expect breaking changes to the Rust API, schema language, and diagnostic set before 1.0. The minimum supported Rust version is 1.86.
Outlint is developed with AI coding agents under human design, specification, and review; the specification is normative and the conformance corpus runs in CI for every change. Details and the contribution policy are in the repository's README.