Terraform for docs - declarative doc↔code drift reconciliation: init · plan · sync · apply.
Project description
knowform
Terraform for your docs. Documentation silently drifts from the code it
describes. knowform treats that drift the way Terraform treats
infrastructure drift: it maintains a recorded state, shows you a plan of what
diverged, and converges the safe direction on apply.
Stdlib-only, zero runtime dependencies. Python-implemented today (the
structural layer uses ast), but corpus-agnostic in concept.
The problem
You write add(a, b) returns the sum in a doc. Someone changes add to add
+ 1. The doc is now a lie, and nothing tells you. Tests guard code against
code; nothing guards prose against code. Docs rot, silently, until a reader
trusts a claim that stopped being true three commits ago.
The model: three states
Like Terraform, knowform reasons over three states per binding:
| State | Where it lives |
|---|---|
| desired | the doc / spec (the prose claim) |
| recorded | knowform.lock (last blessed) |
| actual | the code (resolved via ast) |
plancompares recorded vs actual and reports drift. Read-only, writes nothing.applyconverges the safe direction only: it regenerates descriptive prose from the code. It never rewrites code to match prose — that direction is surfaced for a human to resolve.syncre-blesses the current world as truth (records intentional divergence), returning bindings toin-sync.
Truth direction is declared, per binding
Every binding declares who is authoritative:
code-is-truth— the doc describes the code. If the code drifts,applycan regenerate the prose. Safe direction.doc-is-truth— the code must satisfy the doc (a spec). If they diverge, the fix belongs in code;knowformrefuses to auto-apply and surfaces it.manual— tracked only, never auto-applied.
Cost model: cheapest-first, zero in steady state
Most runs spend zero tokens. Work is gated in tiers, cheapest first, so the LLM judge only ever sees the survivors:
- Hash gate (free) — normalized SHA-256 of each bound region vs the recorded hash. Unchanged regions stop here. Most runs stop here.
- Structural blast-radius (free) — an in-memory
astgraph (IMPORTS / CALLS, walked in reverse) finds which docs a code change could plausibly reach. Everything off the frontier isin-sync. - LLM judge (opt-in, paid) — only bindings that survive both gates reach
the judge, and only if you wire one. With no judge, survivors are reported
needs-judgeand no tokens are spent.
The steady state — nothing changed — costs nothing.
Quickstart
pipx install knowform # or: pip install knowform
Onboard an existing repo, then run the steady-state loop - the same order as
Terraform's init → plan → apply:
knowform init # discover doc↔code bindings -> knowform.init.json
knowform init --write # materialize the reviewed proposal
knowform plan # report drift (read-only)
knowform sync # bless the current world into knowform.lock
knowform apply # regenerate prose in the safe direction
Wire the optional LLM judge/generator with the extra:
pipx install "knowform[judge]"
knowform apply --anthropic
Adopting a repo: init
knowform init is where you start. It discovers candidate doc↔code bindings
for an unwired repo and proposes them for review — the plan → apply
philosophy applied to onboarding: propose, never enforce.
knowform init # scan the repo -> knowform.init.json
$EDITOR knowform.init.json # keep the good bindings, drop the wrong ones
knowform init --write # record bindings in knowform.bindings.json (docs untouched)
knowform sync # bless the newly managed world
init is read-only over your repo — the only file it writes is the
reviewable knowform.init.json. Nothing is materialized until you run
--write.
Discovery is deterministic and precision-first:
- Docstrings — every documented function/class/method becomes a candidate
(
code-is-truth; the docstring is the governed region). - Markdown references — backtick and call-shaped tokens in unmanaged
.mdthat resolve to exactly one symbol become candidates. - Ambiguous or unresolved references never bind silently; they land in an
unmatchedlist for you to resolve by hand.
Direction is always proposed as code-is-truth; doc-is-truth is never
auto-assigned (declaring a spec is a human decision).
Optional: LLM disambiguation
References that are ambiguous (a name shared by several symbols) can be resolved by a model. This is opt-in — the default run spends zero tokens.
knowform init --llm # use your local Claude Code login (no API key)
knowform init --anthropic # use the Anthropic API instead
--llmshells out to your installedclaudeCLI, so it authenticates with whatever you are already logged in with — a Claude subscription included. It auto-detects the binary and, if it is missing, tells you rather than silently falling back. No install beyond Claude Code itself.--anthropiccalls the Anthropic API directly; it needsANTHROPIC_API_KEYand the judge extra (pipx install "knowform[judge]"). Best for CI, where no interactive login exists.
Either backend only picks among the enumerated candidates — it can never invent
a symbol, and its direction hints are clamped to manual.
Bindings
Bindings live out-of-band in knowform.bindings.json at the repo root, so
your docs and code stay free of any knowform markup — no frontmatter, no fences.
This matters when docs are fed to LLMs or agents as context: zero binding tokens
in the prose. There are two kinds:
{
"version": 1,
"docstrings": [
{ "governs": "calc.py", "symbol": "def add", "direction": "code-is-truth" }
],
"markdown": [
{ "doc": "guide.md", "heading": ["Calc", "Behavior"], "block": 1,
"governs": "calc.py", "code_anchor": "def add",
"direction": "code-is-truth" }
]
}
- docstring bindings track a Python symbol's docstring against its own code.
- markdown bindings anchor a doc region by its heading path (exact
heading text, top-down) plus an optional 1-based
block(a paragraph within the section; omit it to bind the whole section). The region is re-resolved from the heading every run, so edits elsewhere never misalign it — and the doc stays plain prose. governsis a file or glob, contained to the repo (paths escaping root surface as errors, never crash);code_anchornarrows to a symbol viaast(def add,class Foo, or a bare name).- A heading that can't be resolved (missing / ambiguous / block out of range)
surfaces as an error, never a silent guess.
doc-is-truthis never inferred.
You rarely write this by hand — knowform init proposes it and init --write
records it.
Ignoring paths
Drop a .knowformignore at the repo root (one path prefix or glob per line,
# comments) to exclude non-corpus docs like test fixtures or vendored trees.
CI and pre-commit
GitHub Action (composite):
- uses: AndyLan0503/knowform@v0
with:
args: plan
pre-commit:
repos:
- repo: https://github.com/AndyLan0503/knowform
rev: v0
hooks:
- id: knowform
Both pin the moving major tag (v0 today), which each release re-points to the
latest compatible version - so these snippets only change at a major bump, never
on routine releases.
License
MIT © 2026 Andy Lan
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file knowform-0.3.0.tar.gz.
File metadata
- Download URL: knowform-0.3.0.tar.gz
- Upload date:
- Size: 56.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c8844fdef91c7d84bdd25623125c342659daa3dcbc02333d12fecd8d25e3d08
|
|
| MD5 |
92b8d74bb8a5f9ad2c5e502ddc16f411
|
|
| BLAKE2b-256 |
3c1ee242adc4963638048f3ffab27948c57b2854ee4a486e619734a224936613
|
Provenance
The following attestation bundles were made for knowform-0.3.0.tar.gz:
Publisher:
release.yml on AndyLan0503/knowform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
knowform-0.3.0.tar.gz -
Subject digest:
5c8844fdef91c7d84bdd25623125c342659daa3dcbc02333d12fecd8d25e3d08 - Sigstore transparency entry: 2185439688
- Sigstore integration time:
-
Permalink:
AndyLan0503/knowform@5149a06dbfd886532d6fef2feb028e1554c52fc1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/AndyLan0503
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5149a06dbfd886532d6fef2feb028e1554c52fc1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file knowform-0.3.0-py3-none-any.whl.
File metadata
- Download URL: knowform-0.3.0-py3-none-any.whl
- Upload date:
- Size: 42.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12cfd1e06ac934e6f0867c65ae938a60d38b1996dd5a9992d10d963c270a70df
|
|
| MD5 |
d926973d179c13da4a4e92417c9b64cf
|
|
| BLAKE2b-256 |
8fad143465f62a80a1c4c4e1cd5e8fb650c42e7f09ef4fba283cd5bc28c9c122
|
Provenance
The following attestation bundles were made for knowform-0.3.0-py3-none-any.whl:
Publisher:
release.yml on AndyLan0503/knowform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
knowform-0.3.0-py3-none-any.whl -
Subject digest:
12cfd1e06ac934e6f0867c65ae938a60d38b1996dd5a9992d10d963c270a70df - Sigstore transparency entry: 2185439888
- Sigstore integration time:
-
Permalink:
AndyLan0503/knowform@5149a06dbfd886532d6fef2feb028e1554c52fc1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/AndyLan0503
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5149a06dbfd886532d6fef2feb028e1554c52fc1 -
Trigger Event:
release
-
Statement type: