Skip to content
Check Guide
On this page

Run selfdoc check to validate directives, measure coverage, execute marked examples, spell-check page prose against the vendored word list and the shared accept list, lint blog posts alongside documentation pages, and apply every registered lint rule with its declared severity -- suppressing warnings only, never errors.

#Check Guide

selfdoc check is your documentation linter. It validates every directive in your templates, measures how much of your public API is documented, runs SEO best-practice checks, and detects stale descriptions. Run it locally before pushing, or wire it into CI for automated enforcement.

#What It Does

A single selfdoc check run performs three categories of analysis that together cover directive correctness, API documentation coverage, and SEO best practices. Each category produces structured output with file paths, line numbers, and actionable messages:

  1. Directive validation -- resolves every directive marker in your .stricttools/docs/ templates and reports whether each one succeeds or fails.
  2. Coverage analysis -- counts public/exported symbols in your source code and checks how many are referenced by directives.
  3. SEO linting -- scans templates for heading structure, meta description, alt text, contrast ratio, and other best practices.
$_ bash
selfdoc check

#Directive Validation

Every directive in your docs is resolved against the actual source code. If a module path is wrong, a target symbol does not exist, or a custom directive script throws an error, the check reports it with file and line number:

Directives
  index.md:12  ref path="mypackage"        OK
  api.md:8     ref path="mypackage.core"    OK
  api.md:20    table-schema path="bad.path" FAILED: Module not found

Fix failures by correcting the directive's path or target attribute to match your actual source code.

#Coverage

Coverage measures how much of your public API surface is documented. selfdoc walks your source directories, extracts public symbols using each entry's language extractor, and checks whether each symbol appears in the resolved content of a directive.

Coverage: 15/23 symbols documented (65%)
          18/23 symbols referenced (78%)
Unreferenced symbols:
  mypackage/core.py: helper_function, InternalConfig
Skeleton-only symbols:
  mypackage/core.py: Pipeline

A symbol is referenced when a directive names it and documented when the source it came from actually says something about it. A symbol that is referenced but not documented is reported as skeleton-only: the page has a heading for it and nothing under it.

#Coverage threshold

Set coverage_threshold in your selfdoc.json to the fraction of public symbols that must be documented. It is a number between 0.0 and 1.0, and it defaults to 1.0 -- full coverage. Below the threshold, selfdoc check prints the shortfall and exits with code 1, which is what makes it usable as a CI check against coverage regressions:

{} json
{
  "coverage_threshold": 0.8
}

#Excluding modules

Modules listed in gen.exclude are excluded from both coverage calculations and auto-generated documentation pages. Use this for intentionally-internal modules, test helpers, or implementation details that should never appear in public-facing documentation. Excluded symbols do not count against your coverage threshold:

{} json
{
  "gen": {
    "exclude": ["mypackage._internal", "mypackage.tests"]
  }
}

#Lint Rules

Every selfdoc check invocation runs the whole lint registry: SEO and page structure, description staleness and source drift, cross-references and symbol documentation, example validation, CLI reference completeness, version consistency, blog posts, and unified sites. Each rule has a unique code, a severity, and an actionable message explaining what is wrong and how to fix it. Errors cause a non-zero exit; warnings are informational. Each code and its severity are declared once, in the lint registry embedded in the binary, and the table below is rendered from it.

Lint Rules
CodeSeverityWhat it checks
SEO001errorMultiple H1 headings on a page. Use a single # heading.
SEO002warningHeading level gaps (e.g., H2 followed by H4 skipping H3).
SEO003warningImage with empty alt text (![](...)). Add descriptive alt text.
SEO004warningThe document title this page renders exceeds 60 characters, which is past what a search result renders. Shorten the page title.
SEO006errorMissing description in frontmatter. Add one for meta tags.
SEO007warningFirst paragraph after a heading is outside the 30-80 word range. Every page type is held to the same band, generated pages included.
SEO008warningLow numeric data density. Pages with 200+ words should include concrete quantities; version strings and calendar years do not count.
SEO009warningDescription is shorter than 110 characters. Aim for 110-160.
SEO010warningFrontmatter description exceeds 160 characters, which is past what a search result renders. Trim it.
SEO011warningEmpty heading section (heading followed by another heading with no content between).
SEO012warningWCAG contrast ratio below threshold for theme colors. Fix in CSS custom properties.
SEO013errorNo title source: neither frontmatter title nor an H1 heading exists on the page.
SEO014warningMeaningless image alt text (e.g., "image", "screenshot", or a bare filename). Write something descriptive.
SEO015warningGeneric anchor text like "click here" or "read more". Use descriptive link text.
STALE001errorPage content changed but frontmatter description was not updated. Review and update the description.
STALE002warningManifest and disk disagree: a page or post exists on disk but is missing from .stricttools/docs-state/manifest.json, or the manifest lists one that is gone. Run selfdoc gen.
DRIFT001errorThe source docstrings (or CLI schema) a page documents changed while its description did not. Update the description, or run selfdoc baseline accept <page> if it is still accurate.
DQ001warningThe frontmatter description restates the page or symbol name instead of describing it.
DQ002warningFrontmatter description is shorter than 20 characters.
DQ003warningA page carrying a ref directive has a description shorter than 30 characters.
XREF001warningA Markdown link points at a .md page that does not exist in the docs tree.
XREF002errorA directive's path resolves but names a file that is not on disk.
PARAM001warningA referenced symbol has a parameter its docstring never documents.
RETURN001warningA referenced symbol returns a value its docstring never documents.
EXAMPLE001warningA Python or JSON code block does not parse. Fix the snippet's syntax.
EXAMPLE002errorA code block marked validate failed its configured validator. The message carries the validator's exit code and output tail.
EXAMPLE003errorA code block is marked validate but no examples command is configured for its language. Add one, or drop the marker.
CLI001warningstrictcli project: a CLI reference page is missing for a command, or a flag in the schema is not documented on its page.
CLI002warningstrictcli project: a command, group, or flag help text is shorter than 50 characters.
LANG001errorA configured source entry names a language selfdoc has no extractor for.
SEARCH001errorpagefind is not installed, so the build cannot index the site.
VER001errorA version listed in versions could not be extracted from its git tag, so it could not be validated.
VER002errorversion in selfdoc.json does not match the version detected from the project manifest (pyproject.toml, package.json, or a VERSION file).
VER003errorThe last entry of the versions array does not match version in selfdoc.json.
VER004errorA generated root file that embeds project.version does not contain the expected version. Regenerate with selfdoc gen --version-override <v>.
SPELL001errorA word in page prose is in neither the vendored English word list nor the accept list. Fix the misspelling, or add the term to ~/Projects/ark/spelling-accept.txt if it is genuine.
POST001errorA post is missing the required date field in its frontmatter.
POST002errorA post is missing the required title field in its frontmatter.
POST003errorA post's date is not written as YYYY-MM-DD.
POST004errorTwo posts resolve to the same slug.
POST005errorA published post's slug changed, which would break its permalink.
POST006errorA post is missing the required directives declaration, or declares something other than true/false. Every post states whether it may carry directive markers; there is no default.
POST007errorA post declaring directives = false carries a directive marker. The message names the marker and the line it sits on.
LINK001errorAn emitted reference -- a link, a canonical, a sitemap entry or a feed link -- names a file the build did not write.
UNIFIED001errorA project listed in the unified section has no selfdoc.json.
UNIFIED002errorA constituent project, or the docs-site's own content, could not be checked.

#Spelling (SPELL001)

Every documentation page and every published post is spell-checked against a vendored English word list of about 172,000 words -- a pinned snapshot of the English Speller Database at its large size, carrying US, British and Canadian spellings, so colour and color are equally correct. The list ships as package data beside upstream's copyright notice, which travels with it as redistribution requires.

Structure comes from the block tokenizer, so fenced code blocks and directive blocks are never scanned; inline code spans, link destinations, URLs, and directive markers are blanked before a line is read. Tokens that look like machinery rather than English -- anything carrying a digit, an underscore, a slash, a dotted qualified name, or an interior capital such as parseConfig -- are skipped whole. Hyphenated compounds are checked part by part, and a possessive is accepted from its base word. Each finding names the file, the line, the column, and an edit-distance-one suggestion when one exists.

Genuine terms the general word list cannot know -- project names, tool names, technical vocabulary -- belong on the accept list at ~/Projects/ark/spelling-accept.txt: one lowercase word per line, # starts a comment, and a word there is accepted in any casing, everywhere. A missing file simply means nothing has been accepted yet. A file that exists but holds a line that is not a bare word is a hard error, so a malformed list is never read as a shorter one.

SPELL001 is error severity and cannot be suppressed. Fixing the prose or accepting the term are the two available answers, which is the point: a misspelling on a published page is a defect, and the accept list records the deliberate decision that a word is not one.

To seed the accept list across a machine, selfdoc spell-corpus runs the same engine over every selfdoc project sitting beside this one and prints each project's unknown words with a first location. It is strictly read-only over the projects it visits.

#Suppressing rules

Suppress specific lint rules globally in your config or per invocation via CLI flags. Both sources are merged, so you can set baseline suppressions in config and add per-run overrides as needed. Use suppression sparingly since each rule catches real SEO or accessibility issues:

{} json
{
  "lint_ignore": ["SEO007", "SEO008"]
}

Or per invocation with --ignore:

$_ bash
selfdoc check --ignore SEO007,SEO008

Both sources are combined -- CLI flags and config are merged.

Suppression reaches warning-severity codes only. Naming an error-severity code -- in lint_ignore or in --ignore -- is a hard error that names the code and its severity, and the run stops before any checking happens. An error says the build is wrong: a broken emitted reference, a missing description, a post whose slug moved. Silencing it hides the defect instead of resolving it, which is how a genuinely broken build once passed its own check. Fix the defect, or change the rule's severity in the registry if the rule itself is wrong.

#Staleness Detection

selfdoc tracks SHA-256 hashes of each page's raw template body (directives unresolved) and its frontmatter description. When the content changes but the description stays the same, it raises a STALE001 error. This catches the common case where you update a page's content but forget to revise the description that feeds into meta tags and search results.

Hashes are stored in .stricttools/docs-state/hashes/hashes.json and auto-committed after each check (unless you pass --no-auto-commit or --dry-run).

#Example Validation

Every Python and JSON code block is parsed during selfdoc check, and a block that does not parse raises EXAMPLE001. Parsing proves only that a snippet is well-formed, not that it still works: an example calling a function you renamed six months ago parses perfectly and is completely wrong. To catch that class, mark the block validate and configure a validator for its language:

`markdown

from mylib import greet

print(greet("world"))

{} json
{
  "examples": {
    "python": "uv run --directory python python {file}",
    "go": "scripts/validate-example-go.sh {file}",
    "ts": "scripts/validate-example-ts.sh {file}"
  }
}

selfdoc writes each marked block to a scratch file suffixed for its language, substitutes the path for {file}, and runs the command from the project root with a 60-second timeout. A non-zero exit becomes an EXAMPLE002 error naming the exit code and the last five lines of the validator's output. A marked block whose language has no configured command becomes an EXAMPLE003 error rather than being skipped, so an unhonored marker can never masquerade as a passing one.

The marker is opt-in per block: unmarked blocks are never executed and keep the EXAMPLE001 syntax check exactly as before. Validators run without a sandbox, so configure commands that compile, type-check, and register rather than ones that execute arbitrary payloads.

#Output Formats

#Text (default)

Human-readable output with colored status indicators, file paths, line numbers, and rule codes. This is the default format designed for local development where you read the output directly in a terminal and fix issues one by one:

$_ bash
selfdoc check

#Machine output

Machine-readable output for CI integration, custom tooling, or programmatic analysis. --json is the framework's machine mode: stdout carries exactly one document, the envelope, and the check report is its payload member. The report holds the same information as the text output, structured as arrays of objects with consistent field names for easy parsing:

$_ bash
selfdoc check --json

The payload is an object with directives, coverage, lints, and exit_code fields. Example structure:

text
{
  "interface_version": 1,
  "app": "selfdoc",
  "command": "check",
  "exit_code": 0,
  "payload": {
    "directives": [{"file": "index.md", "line": 12, "status": "OK", ...}],
    "coverage": {"total_public": 23, "referenced": 15, ...},
    "lints": [{"code": "SEO006", "severity": "error", ...}],
    "exit_code": 0
  }
}

The payload's shape is declared as a JSON Schema on the command itself and validated before it is written, so a document that deviates fails the run instead of reaching a consumer. selfdoc --dump-schema publishes the declaration.

#Exit Codes

selfdoc check uses standard exit codes to signal pass or fail, making it safe to use as a CI gate or pre-commit hook. It exits with code 0 when everything passes, and code 1 when any of these conditions are true:

  • A directive resolution failed
  • Any lint has severity error
  • Coverage is below the coverage_threshold fraction

Warnings alone do not cause a non-zero exit. This makes it safe to use in CI -- warnings are informational, errors block the pipeline.

Tip

Run selfdoc check --dry-run to see staleness results without writing hash files to disk. Useful for previewing what would change.

Next: rlsbl Integration

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search