Skip to main content

Tiredize

Run tests and upload coverage Coverage Status

Tiredize is a schema-driven markdown validation and linting tool. It parses markdown documents into a structured representation and validates them against user-defined schemas and configurable lint rules. Define what your documents should look like, and tiredize tells you where they don't.

The tool was built to enforce quality control on Technique Research Reports published by TIRED Labs, but it is general-purpose. Document structure is defined via external YAML configuration files, so any project with structured markdown documentation can use it.

Features

Markdown schema validation -- Define the expected section structure of a document: which sections must exist, their heading levels, their ordering, whether sections are optional or repeating, and section name matching via exact string or regex. Tiredize validates the document against the schema and reports missing, unexpected, misordered, or incorrectly leveled sections. Supports both ordered and unordered validation modes.

Linter rules -- A pluggable rule engine for style and formatting checks. Built-in rules cover markdown element restrictions, line length, tab usage, trailing whitespace, unicode characters, and link validation (including HTTP checks, anchor resolution, and relative file path verification). Advanced users can add custom rules by modifying the built-in rules package (for example, via an editable install or project fork).

Markdown parser -- A regex-based parser that extracts headers, sections, code blocks (fenced and inline), links, images, tables, block quotes, and frontmatter into typed dataclass elements with accurate position tracking. Links include the inline and reference-style forms as well as GFM autolinks. An autolink is a <url> or <email> in angle brackets. An extended autolink is a bare URL, a www. address, or an email address written in plain prose. Autolink recognition follows the GFM specification. The exact recognition rules are in the markdown parser specification. List extraction is planned but not yet implemented.

Frontmatter schema validation -- Validate YAML frontmatter fields against a user-defined schema. Declare which fields must exist, their expected types (string, int, float, bool, date, list), and optionally constrain their values to a set of allowed entries. Detects duplicate YAML keys, rejects map values, and enforces string-only list items with no duplicates.

Installation

Requires Python 3.10 or later.

Releases are published to PyPI. pip install tiredize installs the latest release there, so it works once at least one release has been published. To install a particular release, pin its version:

pip install tiredize
pip install tiredize==X.Y.Z
tiredize --version

How releases are numbered and published is described on the releasing page.

For development, install from a clone of the repository:

git clone https://github.com/tired-labs/tiredize.git
cd tiredize
pip install -e .
pip install pytest pytest-cov flake8

No file in the repository holds a version number. The version is derived from the repository's git tags when tiredize is built or installed, so use a full clone. git clone fetches the tags, but a shallow clone (--depth) may lack them and then builds a dev version that says nothing useful about where the code stands. A source tree with no git metadata at all, such as a GitHub "Download ZIP" source download, fails to build rather than invent a number.

An editable install records the version current when it ran. tiredize --version keeps reporting that version after later commits or tags, until you run pip install -e . again.

Usage

Tiredize runs from the command line. It accepts markdown files as positional arguments and configuration via flags. There are two equivalent ways to invoke it:

tiredize [OPTIONS] [PATHS...]
python3 -m tiredize [OPTIONS] [PATHS...]

The installed console script and module execution take the same arguments and produce the same output, on the same streams, with the same exit status. Module execution is the safer choice in a pre-commit hook or a CI job, where the console script may not be on PATH. Write python -m tiredize only where python is known to be the Python 3 interpreter tiredize was installed into — many systems ship python3 and no python at all.

The examples below use the console script.

Validate document structure against a schema

tiredize --markdown-schema schema.yaml document.md

Run linter rules

tiredize --rules rules.yaml document.md

Validate frontmatter against a schema

tiredize --frontmatter-schema frontmatter.yaml document.md

Combine all three

tiredize --markdown-schema schema.yaml --frontmatter-schema frontmatter.yaml --rules rules.yaml document.md

Multiple files

tiredize --markdown-schema schema.yaml docs/*.md

For each file, in the order the paths were given, the command prints that file's findings to stdout as file:line:col: [rule_id] message, or a single file: no issues found. line if the file produced none. Runtime errors go to stderr as error: <message>.

Print the installed version

tiredize --version

This prints one line to stdout, tiredize X.Y.Z, and exits 0. The version is that of the installed package. It is a release number such as 0.1.0, or a dev version such as 0.1.1.dev3 when tiredize was installed from an untagged commit. If tiredize runs from a source tree that was never installed, the line reads tiredize unknown.

Like --help, --version takes effect as soon as it is read. Any configuration flags and paths given with it are neither checked nor loaded.

Exit status

Every invocation exits with one of exactly three statuses, which is what makes tiredize suitable for pre-commit hooks and CI/CD pipelines. Both invocation forms return the same status for the same arguments.

Status Meaning
0 Every file was read and produced no findings. --help and --version also exit 0.
1 At least one file produced findings, or a runtime error occurred.
2 Usage error: no files were given, or none of --rules, --markdown-schema, and --frontmatter-schema was given. An unrecognized flag, or a flag missing its value, also exits 2.

Status 1 covers two different things, which differ in what they do to the rest of the run.

Findings are what a check reports about a document: a lint rule violation, a markdown schema mismatch, or a frontmatter schema violation. Findings do not stop the run — every file is processed, every finding is printed, and the status is 1 at the end.

Runtime errors are what stop a check from running at all: a file that does not exist, a configuration file that is missing or unparseable, an unknown rule ID, an invalid rule configuration, or an ambiguous markdown schema. A runtime error does stop the run. The first one prints to stderr and tiredize exits 1 immediately, leaving every remaining file unprocessed.

Errors abort and findings continue, whichever way you invoke the tool. In the run below, if missing.md cannot be read, tiredize reports that error and never looks at guide.md:

tiredize --rules rules.yaml intro.md missing.md guide.md

So the output of a run that exited 1 is a complete report only when the 1 came from findings. A 1 arriving with an error: line on stderr means the remaining files were never examined.

Configuration

Markdown Schema

A YAML file defining the expected section structure. Sections can be required or optional, matched by exact name or regex pattern, and allowed to repeat with min/max bounds. Nested sections are supported.

# Enforce that documents have these sections in order
enforce_order: true
allow_extra_sections: false

sections:
  - name: "Introduction"
    level: 1
    sections:
      - name: "Background"
        level: 2
      - name: "Scope"
        level: 2
        required: false
  - name: "Methods"
    level: 1
    sections:
      - pattern: ".+"
        level: 2
        repeat:
          min: 1
  - name: "Results"
    level: 1
  - name: "References"
    level: 1

See the markdown schema validator specification for the full format reference, including all properties, constraints, and validation algorithm details.

Frontmatter Schema

A YAML file defining expected frontmatter fields, their types, and optionally their allowed values.

fields:
  status:
    type: string
    allowed:
      - draft
      - ready
      - active
      - done

  priority:
    type: string
    allowed:
      - critical
      - high
      - medium
      - low

  created:
    type: date

  tags:
    type: list
    required: false

See the frontmatter schema validator specification for the full format reference, including all properties, type mapping, constraints, and error types.

Linter Rules

A YAML file where each top-level key is a rule ID and its value is the rule's configuration. Only rules with an entry in the config file are enabled.

Every rule declares the configuration keys it accepts and which of them it requires, and its configuration is checked before the rule runs. Three things are errors, not warnings:

  • a key the rule does not accept — a typo, usually;
  • an accepted key holding a value of the wrong type;
  • a required key that was omitted.

Each of the three prints a message to stderr naming the rule and the offending key, then aborts the run with exit status 1:

error: Rule 'line_length': unknown configuration key 'max_length'. Accepted keys: exclude, maximum_length.

Omitting an optional key is fine — the rule falls back to its own default. The Required column in the tables below says which keys are which. Required means the key must be present, not that it must be enabled: links: {validate: false} is a legitimate way to turn link checking off.

line_length

Flags lines that exceed a maximum character count. Line endings and newline characters are excluded from the count. Length is measured in Unicode characters, not bytes.

Option Type Required Description
maximum_length int Yes Maximum allowed line length in characters.
exclude list No Element types whose lines are skipped. See Recognized markdown element names. Any line that overlaps with a listed element is exempt. Defaults to no exemptions.
line_length:
  maximum_length: 80
  exclude:
    - table
    - link_inline

tabs

Flags tab characters anywhere in the document.

Option Type Required Description
allowed bool No When false, any tab character is a violation. Tabs are forbidden when the key is omitted.
tabs:
  allowed: false

trailing_whitespace

Flags lines that end with one or more whitespace characters before the line ending.

Option Type Required Description
allowed bool No When false, trailing whitespace on any line is a violation. Trailing whitespace is forbidden when the key is omitted.
trailing_whitespace:
  allowed: false

unicode

Enforces whether unicode (non-ASCII) characters are permitted in the document. Use exclude to carve out element types that are treated opposite to the allowed setting — for example, to forbid unicode everywhere except inside code blocks.

Option Type Required Description
allowed bool Yes When true, unicode is permitted throughout the document. When false, unicode is forbidden. The key selects the mode the rule runs in, so it must be given.
exclude list No Element types that are treated opposite to allowed. See Recognized markdown element names. Defaults to no exemptions.
# Forbid unicode everywhere except in code blocks and inline code
unicode:
  allowed: false
  exclude:
    - code_block
    - code_inline
# Permit unicode everywhere except in headers
unicode:
  allowed: true
  exclude:
    - header

Validates that URLs in the document are reachable. Checks inline links, autolinks, extended autolinks, and reference definitions. Anchors (#slug) are resolved against section headings in the document. Relative paths are checked for file existence on disk.

For every one of those link kinds, only http and https targets are checked. A link with any other scheme is recognized as a link but never produces a finding. That holds for mailto: and xmpp: addresses, for ftp: and irc: URLs, and for any unregistered scheme. A www. link is checked with http:// prepended, so www.example.com is requested as http://www.example.com. A ./path or \path written in prose is no longer treated as a link at all. Relative paths are still checked when they appear in an inline link or a reference definition.

The scheme check goes by shape rather than by a list of known schemes, so a target whose first token looks like a scheme is now skipped silently for every link kind. That includes localhost:8080, example.com:8080/x, and C:\path. Write http://localhost:8080 to have such a target checked.

Option Type Required Description
validate bool Yes Enable link validation. When false, no links are checked.
timeout int No Timeout in seconds for HTTP requests.
headers dict No HTTP headers to include in every request (e.g. Authorization).
valid_status_codes list No HTTP status codes treated as valid. Defaults to all 2xx and 3xx codes. Entries may be exact integers (200) or class wildcards (2xx, 3xx).
exclude list No Domain patterns to skip. Supports * as a wildcard (e.g. *.mycompany.com). Relative paths and anchors are unaffected.
links:
  validate: true
  timeout: 5
  exclude:
    - "*.mycompany.com"
    - mycompany.atlassian.net

elements

Prohibits specific markdown element types from appearing in the document. Each occurrence of a disallowed type is reported as a separate violation.

Option Type Required Description
disallow list Yes Element types that must not appear. See Recognized markdown element names. The rule inspects nothing without it.
elements:
  disallow:
    - link_inline
    - autolink_extended

Recognized markdown element names

The following names are valid in exclude and disallow lists:

Name Description
autolink Autolink (<url> or <email>)
autolink_extended Extended autolink (bare URL, www., email, mailto:/xmpp:)
code_block Fenced code block
code_inline Inline code
header Section header
image_inline Inline image
image_reference Reference-style image
link_inline Inline link
link_reference Reference-style link
quoteblock Block quote
reference_definition Reference link definition
table Pipe-delimited table

Earlier versions named the two autolink types link_bracket and link_bare. Neither name is accepted any more. A rules file that still uses one fails at load, before any document is checked, with error: Unknown element name in disallow: 'link_bare' on stderr (or … in exclude: … for a line_length or unicode list) and exit status 1. The fix is to rename link_bracket to autolink and link_bare to autolink_extended.

Custom Rules

Tiredize discovers linter rules automatically from Python modules. To add a custom rule:

  1. Create a Python module (e.g., my_rule.py) that declares the configuration keys it accepts and exposes a validate function:

    from tiredize.core_types import Position, RuleResult
    from tiredize.linter.utils import validate_config
    from tiredize.markdown.types.document import Document
    
    # The name this rule reports configuration errors under, every
    # key it accepts mapped to its type, and the subset it requires.
    _RULE_ID = "my_rule"
    _ALLOWED_KEYS = {"maximum_count": "int"}
    _REQUIRED_KEYS = ("maximum_count",)
    
    def validate(
        document: Document,
        config: dict,
    ) -> list[RuleResult]:
        validate_config(config, _ALLOWED_KEYS, _REQUIRED_KEYS, _RULE_ID)
    
        results = []
        # Your validation logic here.
        # Return RuleResult instances with rule_id=None
        # (the engine fills it in from the module name).
        return results
    

    The validate_config() call is not optional, and it belongs at the top of validate(), before any key is read. It raises ValueError for the three faults described under Linter Rules: a key the rule does not accept, an accepted key holding a wrong-typed value, and an omitted required key. A rule that skips the call reads a mistyped key as an absent one and then silently checks nothing, which is the failure the call exists to prevent.

    _RULE_ID names your rule in those messages, and it is the only place the rule's ID is written by hand: the engine never reads it, and derives the ID it stamps on findings from the module filename. Keep the two equal. Set _RULE_ID to anything else and a configuration mistake is reported against an ID that appears nowhere in your rules file, while findings from the same rule carry the filename.

    Declare a key as required when the rule does nothing without it, and optional when the rule can fall back to a default and still do its job. Each key's type is named with one of bool, dict, int, list, or str. A rule that accepts no keys at all still calls validate_config(), passing an empty _ALLOWED_KEYS; that is what makes configuring such a rule an error rather than a no-op.

  2. Place the module in the built-in rules package (tiredize/linter/rules/). This requires an editable install (pip install -e .) or a project fork. The module must be non-private (no leading underscore) and expose a validate function. Discovery checks nothing beyond that, so nothing stops a rule that omits validate_config() from loading — the convention is yours to keep.

The rule ID is derived from the module filename (e.g., my_rule.py produces rule ID my_rule). Configuration values for your rule are passed via the config dict from the YAML file.

See the linter specification for the full rule pattern and available configuration helpers.

License

GPL-3.0

Metadata

Release files for tiredize 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tiredize 0.1.0
File Size Uploaded Provenance Actions
tiredize-0.1.0.tar.gz 208.2 kB With provenance Details for tiredize-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tiredize 0.1.0
File Interpreter ABI Platform Actions
tiredize-0.1.0-py3-none-any.whl Python 3 none any Details for tiredize-0.1.0-py3-none-any.whl

Total release size: 284.5 kB

Release files / tiredize-0.1.0.tar.gz

Download URL tiredize-0.1.0.tar.gz
Size 208.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f43bbe6879e73f9cd737ba034b3d204bbca8e3aa374b89c7bfabb5b6716aa250
BLAKE2b-256 checksum
How to use checksums
65be3d9254ae6e67d8eaa3cabaea02f5292b25585d4855aa76f402a037141570
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

Release files / tiredize-0.1.0-py3-none-any.whl

Download URL tiredize-0.1.0-py3-none-any.whl
Size 76.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6faf83e95e1ea7402b26eaab3b087d9dfd329c29a9995ce9a228d020df8d5865
BLAKE2b-256 checksum
How to use checksums
eab2c0e92d47723b2ce87b776fe0e8e8ebd72ca0113e6146ef45391dcec1dd12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

Security for tiredize 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tiredize-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page