Skip to main content

constant-docs

Documentation that writes and maintains itself.

A command-line tool and a Python library. It runs in any repository, in any language, driven by whatever coding agent you already use.

Agents ship code faster than anyone can keep the docs up to date.

Each document records a hash of the code behind it. Change that code and the document goes stale. constant-docs names what fell behind, and your agent rewrites it.

$ constant-docs verify
Stale modules: src/payments

How the loop works

Edit a file, and the document describing it is already behind. The loop closes inside the same turn:

$ constant-docs verify
All modules up to date.

$ echo 'def refund(charge_id: str) -> None: ...' >> src/payments/core.py

$ constant-docs verify
Stale modules: src/payments

$ # your agent reads the source, rewrites docs/src/payments.md, and:

$ constant-docs verify
All modules up to date.

Your coding agent writes every word, because it already holds the source and a model connection. constant-docs names the documents that moved and takes the new text back.

That one command is also the whole CI integration. It exits 1, and a change that alters behaviour while leaving its documentation behind does not merge:

- run: constant-docs verify

Install

pip install constant-docs        # or: uv add constant-docs

One dependency, PyYAML. Python 3.11 or newer.

Try it

cd examples/sample-repo
constant-docs verify
Missing documents: src/ledger, src/payments

Point it at your own repository with a constant-docs.yaml at the root:

version: 1
docs_root: docs

modules:
  src/payments: src/payments/**/*.py
  src/ledger: src/ledger/**/*.py

constant-docs verify now names what is missing.

Every command and flag is in the CLI reference; every configuration key is in the configuration reference. Neither is restated here, because a second copy of the thing that changes most is the copy that goes stale.

Driving it from your agent

Three commands carry the whole integration, and none of them names a particular tool:

Command Run it It does
constant-docs mark <path> after your agent writes a file notes which documents that path affects. A glob match and a dictionary insert
constant-docs settle --json when your agent goes quiet names what the marks turned up, and hands over each document's files, its previous text, and the conventions to write against
constant-docs apply <module> "<body>" once your agent has written stores the text and moves the hash

A shell script drives that loop. So does a Makefile, a git hook, or a person:

$ git diff --name-only | xargs constant-docs mark
$ constant-docs settle --json

settle reports what mark recorded, so a harness that skips the marking step sees an empty list. plan --json considers every module, and verify hashes everything and exits 1 on drift whatever the marks say. Keep verify as the gate.

Claude Code gets a plugin. Install it from plugin/ and two hooks run those commands for you: PostToolUse runs mark, Stop runs settle --hook. Edit a file, stop the turn, and its document is up to date. Nobody asked. Any harness that can run a command on write and at turn end does the same job.

Regeneration happens at quiescence, never mid-edit, so the generator and the coding loop never write the same file at once. The dirty set lives in .constant-docs/dirty.json — gitignored — so a crashed session is picked up on the next run.

The harness guide walks through each route.

Kinds

A document is not always a module explainer. Its kind decides which sections it must carry, which prompt its generator is handed, and whether a regeneration replaces the body or appends to it.

Kind For Mode
module A set of source files. The default replace
spec What a subsystem promises, and why replace
log A changelog or build log append
architecture The module map, with a diagram replace
errors Every message the tool can emit replace
config-reference Every configuration key replace
cli-reference Every command and flag replace
modules:
  src/payments: src/payments/**/*.py    # kind `module`, the default

  SPEC:
    glob: ["src/**/*.py", "tests/test_*.py"]
    kind: spec

  SECURITY:
    covers: [src/payments, src/web]     # follows when either glob moves
    kind: spec

An append never rewrites or reorders what is already there. Pass a whole body to a log, or a single entry to a module document, and the tool refuses it. A configuration that has never heard of kinds behaves exactly as it did.

One house style governs every generated document. It lives in a single file that all seven prompts receive, so the rules cannot drift apart.

Finding what is undocumented

constant-docs init          # what is here, and what covers none of it
constant-docs coverage      # source no module covers, by directory

verify can only check what has been declared, so a repository could otherwise drift to a large fraction undocumented while every check passed. coverage does one thing: a set difference. Declare the directories that should carry no document in configuration, with a reason each. An exclusion nobody justified is one nobody decided.

verify --coverage folds it into the gate, opt-in. And init never removes: it proposes, because dropping a module orphans its document and prune deletes it.

Already have a docs/ folder? Pointing the tool at it is safe. prune deletes only documents carrying this tool's frontmatter. Your hand-written decision record belongs to somebody else, so the tool never touches it. It names those files on every run, so you never mistake silence for "there was nothing else". It still reports them, because the docs root is a bundle and they do not conform to it. Refusing to check a file and refusing to delete it are two different things.

Running it on a schedule

Hooks need a live session. Changes also arrive from a timer, a chat client, or somebody editing files directly, and nothing fires for those. auto runs a command you declare, then checks what it left behind:

auto:
  command: "<your agent> -p 'Run the constant-docs settle loop'"
  when: stale
  budget: 5

The tool still calls no model. constant-docs splits your command with shlex and runs it directly, so an agent CLI, a script and a Makefile target all work. No credential goes in it, and the command's success is checked: auto re-runs verify afterwards. It exits 0 when everything is clean, 1 when something is still stale, and 2 when the command could not run. Three outcomes, because a scheduler that confuses the last two retries the wrong one.

Three things it checks that a hash cannot

A regeneration cannot drop a decision. A document rewritten by a model can lose the thing it existed to record. Give each recorded decision an identifier and the tool refuses a rewrite that loses one, naming it. Rewording is always free; only losing is not. Retiring one is deliberate and gets written into the document.

A cross-cutting document follows what it covers. covers: [api, cli] takes in those modules' files by name, so it goes stale when their boundary moves.

An error catalogue agrees with the source, both ways. Every message the code raises appears in the catalogue, and every entry still exists in the code. Neither needs parsing beyond a string scan, so it costs nothing — and it is the one document here the tool can actually guarantee. The rest are only fresh.

What a document looks like

---
type: Code Module
title: Payments
description: Reconciles inbound payment events against ledger entries.
tags: [code-module]

constant_docs:
  module: src/payments
  kind: module
  source_glob: src/payments/**/*.py
  source_files:
  - src/payments/core.py
  - src/payments/reconcile.py
  source_hash: sha256:a3f1c07e…
  hash_method: sha256-over-sorted-path-and-content
  hash_covers: source_files
  timestamp: '2026-08-19T09:14:00Z'
  generator: constant-docs/0.4.0
  generator_spec: https://github.com/ashborn-systems/constant-docs/blob/main/docs/SPEC.md
---
## Purpose

Owns the mapping between external payment events and internal ledger entries.
Callers submit a batch and receive a report. Matching strategy, retry policy,
and duplicate handling are internal.

## Correctness pillars

WHEN a batch contains a duplicate event ID, the module SHALL discard the later
event and record it in the report.

## Known failure modes

| Symptom | Cause | Fix |
|---|---|---|
| Report totals disagree with the ledger | Batch submitted while a ledger write was in flight | Submit batches only from the settled-state queue |

The frontmatter stands alone. An agent that has never run this tool can see what the document describes, where its source lives, and how to recompute the digest. The tool keeps everything it owns under one constant_docs key, an Open Knowledge Format producer extension. type, title and tags stay yours, and unknown keys survive a write.

Python API

from constant_docs import append, apply, plan, prune, settle, verify

p = plan(changed_paths=["src/payments/reconcile.py"])
apply("src/payments", body="## Purpose\n…")  # writes only if the body differs
append("CHANGELOG", entry="…", title="0.3.0")
report = verify()  # no writes, no network
prune()  # delete orphaned documents

What it does not do

No model client, no network call, no source parsing. No AST, no tree-sitter, no import graph. Modules are globs and files are bytes, which is what makes "any language" a property here.

It cannot catch writing about the world outside your files. A document saying a disk is half full stays fresh for ever, because nobody edited anything. Hash what lives in files. Leave live values to something that owns them.

It cannot catch two documents contradicting each other. Content hashing sees one thing: a document falling behind its source. That is why the tool always moves a section and never copies it.

And it cannot catch a decision reworded into something weaker. The guard draws one line, between present and deleted. Degradation is left to review, and said so here so that you learn it now.

It ships one ready-made integration. The Claude Code plugin installs and runs; for any other agent you wire mark and settle to whatever signals it gives you, which takes a few lines of shell.

This file maintains itself. It carries the tool's own block in an HTML comment, which GitHub renders as nothing. It goes stale whenever the CLI, the kinds or the Python API move.

Reading further

Licence

Apache 2.0 — see LICENSE.

Release files for constant-docs 0.4.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 constant-docs 0.4.0
File Size Uploaded
constant_docs-0.4.0.tar.gz 92.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for constant-docs 0.4.0
File Interpreter ABI Platform
constant_docs-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 203.8 kB

Release files / constant_docs-0.4.0.tar.gz

Download URL constant_docs-0.4.0.tar.gz
Size 92.4 kB
Tags Source
SHA-256 checksum
How to use checksums
6b54589066d58e6fc248e0c601456e2e9115d9d22e4cab8c642d3cca0e740017
BLAKE2b-256 checksum
How to use checksums
4c10709201256375aef5906fe116279a9393bc601221de7785e64d4e9a45ad2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 26, 2026.

Transparency log

Release files / constant_docs-0.4.0-py3-none-any.whl

Download URL constant_docs-0.4.0-py3-none-any.whl
Size 111.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eb8301b0027fc46b693c5a636131bb3be47b04e1d1704048a80271a97201fece
BLAKE2b-256 checksum
How to use checksums
2f288f9897fff89cfd5455aa8c4dc389949c8b14dba9c6e755414a45395e4612
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 26, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

This release

0.4.0 This release

2 release files

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