Skip to content
Staleness Detection
On this page

How selfdoc detects stale frontmatter descriptions by hashing page content, how to fix STALE001 errors, and how to accept reviewed dead-ends.

#Staleness Detection

You update a page's content but forget to update the frontmatter description. Now your meta tags, OG cards, and search snippets show outdated text. selfdoc catches this automatically by tracking content hashes.

#The Problem

Frontmatter descriptions feed into <meta name="description">, Open Graph tags, Twitter cards, search index summaries, and the Atom feed. When the page body changes significantly but the description stays the same, those external-facing representations go stale. Users see one thing in search results and something different on the page.

This is easy to miss during normal editing. You change the content, the page looks fine, and you move on -- never noticing the description no longer matches.

#How It Works

selfdoc maintains a hash store at .stricttools/docs-state/hashes/hashes.json that tracks each page's content and description independently. By comparing current hashes against stored baselines on every selfdoc check run, it detects when content has drifted from its description. For each documentation page with a frontmatter description, it tracks two SHA-256 hashes:

  • Content hash -- computed from the page's raw template body: frontmatter stripped, directives left unresolved, and each directive marker's attribute values canonicalized. A directive whose output changes (a version bump, a renamed symbol) therefore does not trip staleness, and neither does a mechanical path="x" -> path="y" rename
  • Description hash -- computed from the frontmatter description string

On each run of selfdoc check, the current hashes are compared against the stored ones. The logic is straightforward:

How It Works
Content changed?Description changed?Result
NoNoOK -- nothing changed
NoYesOK -- description updated independently
YesYesOK -- both updated together
YesNoSTALE -- content changed but description did not

Only the last case triggers a staleness error.

#The STALE001 Error

When staleness is detected, selfdoc check reports an error-level diagnostic with the affected filename and a clear message explaining that the content changed but the description did not. This is classified as an error (not a warning) because stale descriptions degrade search results, social cards, and AI summaries:

STALE001 [error] getting-started.md: content changed but frontmatter
description was not updated (possible stale description)

This is an error, not a warning -- it causes selfdoc check to exit with code 1. If you have checks in CI or in an rlsbl pre-checks hook, stale descriptions block the pipeline.

#Fixing It

Update the frontmatter description to reflect the current page content, then run selfdoc check again to record the new baseline hashes and clear the error. Aim for 110-160 characters that accurately summarize what the page covers after the content change:

M markdown
++
title = "Getting Started"
description = "Install selfdoc, initialize a project, and build your first documentation site in under five minutes."
++

Then run selfdoc check again. The hashes update and the error clears.

#Accepting a Reviewed Dead-End

Sometimes the content changed but the existing description is still accurate -- a generated page whose listing order shifted, or a prose correction that does not change what the page is about. Use selfdoc baseline accept to advance the baseline:

selfdoc baseline accept en/index.md en/cli-index.md

selfdoc baseline accept <page> [<page>...] is a deliberate, auditable action that means "reviewed: the content changed, and the existing frontmatter description is still accurate." It advances each named page's baseline to the current content and description hashes -- exactly as if the description had been rewritten -- so the next selfdoc check passes without touching the description.

Acceptance is intentionally per-page and unforgiving:

  • Name each page explicitly, exactly as it appears in selfdoc check output (e.g. en/cli-index.md). There is no --all, no glob, and no --force.
  • Accepting a page that does not exist, has no recorded baseline, or is not currently reporting STALE001 or DRIFT001 is a hard error -- "nothing to accept" never silently succeeds.
  • The same guardrails apply to DRIFT001 (source-docstring and CLI-schema drift); accepting advances every tracked hash for the page.

Like selfdoc check, the command commits the updated .stricttools/docs-state/hashes/hashes.json by default; pass --no-auto-commit to stage the change for a larger manual commit.

#Hash Storage

Hashes are stored in .stricttools/docs-state/hashes/hashes.json, a JSON file mapping each page path to its content and description SHA-256 hashes. The file is written atomically -- to a temporary file, then renamed over the old one -- so an interrupted run cannot leave a half-written store. Commit it: it is the baseline for future comparisons.

{} json
{
  "getting-started.md": {
    "content": "a1b2c3d4...",
    "description": "e5f6g7h8..."
  }
}

Without it, every page looks new and no staleness is detected.

#Store schema version

The store carries a _hash_version field. When the meaning of the stored hashes changes, the version is bumped and any older store is discarded wholesale and re-baselined on the next selfdoc check (nothing is silently reused). Two bumps so far:

  • v1 -> v2 switched the content hash from resolved output to the raw template body, so directive output changes (like a version bump) no longer trip staleness.
  • v2 -> v3 added a per-page seed_hash and canonicalizes directive marker lines before hashing, so a pure path="x" -> path="y" rename no longer changes the content hash.

Because a bump discards the old store, run selfdoc check once after upgrading to re-record the baseline. (Releases run selfdoc gen then selfdoc check, so this happens automatically.)

#Description ownership (seed_hash)

Descriptions are handwritten; machine-emitted text is only ever a placeholder. The store records a per-page seed_hash -- the SHA-256 of the description text selfdoc gen last emitted for that page -- so selfdoc can tell a machine placeholder apart from a hand edit:

{} json
{
  "gen-index.md": {
    "content": "a1b2c3d4...",
    "description": "e5f6g7h8...",
    "seed_hash": "9a8b7c6d..."
  }
}

Ownership of the store fields is split by writer: selfdoc gen owns seed_hash, while selfdoc build/selfdoc check own content, description, and the drift hashes. Each writer merges rather than overwriting, so they never clobber the other's fields.

This is what lets selfdoc gen safely regenerate: a description is reseeded only when it is machine-owned (it matches the recorded seed_hash or a known machine template), and a description you rewrote by hand is preserved -- even if a stale seeded: true marker was left in the frontmatter. The same predicate drives the STALE001/DRIFT001 exemption: only genuinely machine-generated descriptions are exempt from the staleness hold, so a generated page you describe by hand is checked like any other page.

#Dry Run Mode

To preview staleness results without updating the hash file on disk, use the --dry-run flag. This computes all hashes, compares them against the stored baselines, and reports any stale pages but does not write changes to .stricttools/docs-state/hashes/hashes.json. Useful for previewing what would be flagged:

$_ bash
selfdoc check --dry-run

This computes all hashes and reports stale pages but does not write to .stricttools/docs-state/hashes/hashes.json. Useful for seeing what would be flagged without changing state.

#The --no-auto-commit Flag

By default, selfdoc check auto-commits hash updates when it writes to the hash file, using the best available commit tool (rlsbl, safegit, or plain git). Use --no-auto-commit to write the updated hashes to disk without creating a commit, which is useful when the hash update is part of a larger change you will commit manually:

$_ bash
selfdoc check --no-auto-commit

This is useful when you want to update hashes as part of a larger change that you will commit manually.

Tip

New pages (ones not yet in the hash store) never trigger STALE001. Staleness is only detected on subsequent runs after the initial hashes are recorded. Run selfdoc check once after adding new pages to establish the baseline.

Next: Glossary

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