🇬🇧 English · 🇩🇪 Deutsch
Consistency checker that verifies a repository's documentation against its actual state — Markdown references (links, images, anchors, identifiers) plus declarations (build targets, version pins, commit trace, planning, workflow references). Deterministic, side-effect-free, shipped as a container image.
d-check checks Markdown documentation as a verifiable invariant network: every machine-decidable documentation invariant is a rule module that can be enabled individually, with its own requirement in the requirements spec — from the reference network (links, anchors, ID link obligations, reference matrix) through Markdown hygiene (span artifacts, host-path leaks), content drift and immutability (content/core pins, git diff) to version-pin, commit-traceability, planning-lifecycle and tracked-status consistency, up to structure invariants within a document:
links— local link and image references: target exists, no repo escape — also for a target address behind a single line break after](and for link reference definitions ([label]: target, checked regardless of use); opt-inresolve-from: files in moving lifecycle directories must resolve every relative target from every location of their group — reported before thegit mv(DC-FA-LINK-001)anchors— heading anchors (GitHub slug procedure) and inline HTML anchors (<a name>,id=) (DC-FA-ANCH-001)ids— link obligation for identifiers (e.g.ADR-NNNN) per declared patterns (DC-FA-ID-001)matrix— reference-direction rules between document classes (DC-FA-MTX-001), within an ordered class (order/direction⇒matrix-downward,DC-FA-MTX-002) and also as a bare ID token in the body (token⇒matrix-forbidden,DC-FA-MTX-003) plus status conditions; exceptions are structural (exclude-sections,allow-supersede-lineage) or declared via the provenance marker<!-- d-check:status-provenance -->external— reachability of external URLs, strictly opt-in (DC-FA-EXT-001)sources— content pin of external sources against upstream drift, opt-in and — besidesexternal— the second network door: a source pinned to asha256(via the marker<!-- source-pin: … -->on itshttp(s)link or the config blocksources:) is fetched, hashed and compared (source-driftwith the full actual hash,source-unreachableon a network failure); single file or archive (unpack: zip) (DC-FA-SRC-001)codepaths— explicit paths in inline code, opt-in; the opt-incheck-linesverifiesfile:<from>-<to>line references (citation-out-of-range,citation-inverted-range) (DC-FA-CODE-001)spans— Markdown span artifacts (unclosed code spans, nested links), opt-in (DC-FA-SPAN-001)hostpaths— host-local absolute and home-relative paths (machine-layout leaks;~/.configand the like stay silent), opt-in (DC-FA-HOST-001)diagrams— identifier existence in diagram fences (e.g.mermaid): every identifier found in the diagram must be defined in itsdefined-insource, opt-in; both valves likeids/codepaths—exempt-paths(file-wide) and the line marker, here a token rather than an HTML comment, and on the fence's opening line for the whole block (DC-FA-DIAG-001)versions— version-pin consistency: pinnedghcrimage references must carry the current version (fromversion.md#aktuell), also reads fenced code, opt-in; the anchor ofcurrent-fromfollows the same answer as inanchors, not the fence exception.versions.patternscarries several pattern/source pairs (your own release and a pinned foreign version) — the short form is the single-element list, and exemptions are per pair (DC-FA-VER-001)pins— content pin against content drift: a link with<!-- dpin: … -->is checked against the hash of its target span (findinglink-staleon drift), opt-in per link; which anchor addresses the span is decided by the same answer as inanchors(duplicate slug, percent decoding, case-sensitive, outside code only) (DC-FA-PIN-001)immutable— immutability pin against core drift: a file with<!-- immutable: … -->is checked against the hash of its normalized core (without the marker line +exclude-sections) (findingcore-drifton drift), opt-in per file; hermetic (no git, read-only working tree) (DC-FA-IMM-001)vcs— git-diff immutability of the core over a commit range: mechanizes the ADR immutability as a distributable module (core-drift-vcs), pure-Go git in the read-only.git(no git binary, no network); withignore-link-targetsa pure path follow-up after a file moved is no drift, opt-in (DC-FA-VCS-001)commits— traceability identifier in commit messages over a range (--range) or the pending message (--commit-msg): every commit message carries aDC-/ADR-/MR-/slice-ID (commit-untraceable), shares the VCS port withvcs, opt-in (DC-FA-COMMITS-001)planning— lifecycle consistency, all three sides. Entry: the idle marker sits in the## Aktuelle Welleblock exactly when noslice-*is in the directory (planning-drift). Exit (additionally opt-in viaclosure.dir): the structure of closure notes on completed work items — section present, enough sentence terminators outside code blocks, no declared boilerplate phrase (closure-note-missing/-thin/-boilerplate); withclosure.recursiveandclosure.skip-patternalso in subdirectories, without archived stubs. Checks structure, not meaning. Hermetic (no git), fail-closed on a missing/ambiguous heading, a missing closure directory and on zero candidates. Wave registers (additionally opt-in viawaves.dir): the roadmap's wave sections against the wave files — active wave ⟺ flat wave document (waves.mode: one, default) or identifier bijection active block ⟺ files with the rest marker staying out of it (waves.mode: many); preview without a file, closed register ⟺ results notes both ways (wave-drift,wave-preview-exists,wave-results-missing,wave-unregistered), opt-in. Register coverage (additionally opt-in viaobservations.registerorobservations.dir, mutually exclusive): a cited observation identifier without proof (observation-unregistered) — a register line in table mode, a<path>/observation.mdfolder in directory mode; prose and link text count, a plain inline-code example does not (DC-FA-PLAN-001)tracked— tracked status of resolvable, existing link/image targets against the git index (target-untracked: an untracked/gitignored target is missing on every fresh clone); index truth (staged = tracked, no.gitignoreinterpretation), reads.gitread-only without a range, opt-in (DC-FA-TRK-001)targets— declaration consistency between docs and build targets: amake Xclaimed in a doc table row without a Makefile rule (gate-phantom), or a Makefile rule without an entry in the authority doc (gate-undocumented); hermetic (no git, no Makefile execution), fail-closed, opt-in; Makefile sources may also be globs (e.g. fragments under anmkdirectory), and the authority doc may be a list of several files, optionally checked for disjointness (gate-declared-twice) (DC-FA-TGT-001)citations— verbatim quote verification: the directive<!-- d-check:cite <path>:<from>-<to> -->marks the following quote (a>-blockquote or inline„…"/"…"; blank lines in between are harmless, a fenced block in between separates); the whitespace-normalized quote must be a contiguous substring of the source span (citation-mismatch); the directive counts only outside fenced blocks and outside inline code — in backticks it is a mention, not a directive —, while the quoted source span is read raw, opt-in (DC-FA-CITE-001)structure— structure invariants within a document: each rule defines a document class via its own globs, a section (literal or RE2) and up to twelve conditions, each with its own reason code — non-empty (section-empty), minimum sentences (section-thin), a line budget of the cleaned text (max-lines⇒section-lines-exceeded, same base set assection-thin— fenced code does not count), task ceiling (section-oversized), open task items on the raw lines (max-open-tasks⇒section-tasks-open, one finding per box on its line — unlikemax-tasksit is immune to the paragraph-wide inline-code pairing, while staying fence-true), plus a conditional required marker (open-tasks-require-marker, only active oncemax-open-tasksalready found excess items): a marker present clears allsection-tasks-openfindings of the section, a missing one replaces them with a singlesection-open-tasks-marker-missing—open-tasks-require-marker-sectionoptionally moves the search into a different, named section of the same file, forbidden and required patterns (section-forbidden,section-pattern-missing), required markers (section-marker-missing), chronological monotonicity of the key column (section-unordered,section-cell-untyped), the shape of every heading in the section (headings-match/headings-level⇒section-heading-mismatch, reported per heading on its own line) and the cell length of a column addressed by its header name (table.column⇒section-cell-oversized/section-cell-undersizedon its own line,section-column-missingfor a column that cannot be addressed); a missing section — or a rule matching no file — yieldssection-missing, several matches undersections: oneyieldsection-ambiguous. Every rule may declare its base set —tasks-ignore-patternremoves task items from themax-taskscount (matched against the item text behind the checkbox, so that^means „the item starts like this"),exempt-section-patternremoves sections from the rule (matched against the same raw heading line assection-pattern,#sequence included). Both only shrink the checked set, carry no reason code of their own, and without them the finding set is byte-identical; if the section exemption empties the set,section-missingis reported rather than silent green — unless the rule states how many it exempts:exempt-expect-count(int ≥ 0, only alongside the pattern) keeps the declared empty set silent as long as the number holds, and otherwise reportssection-exempt-mismatch— in both directions and independently of whether sections remain. The configuration defect (the selector matches nothing) stayssection-missing, even with the number set. The number applies per file, its finding aborts that file, and it ages like any other authored text. Andhintlets a rule write its own explanation — what the reader should do is known only to the rule; the reason code states the kind of defect. Hermetic, opt-in; the closure-note structure of moduleplanningis a preset of the same semantics (DC-FA-STRUCT-001)file— line/byte ceilings of an entire file, independent of file kind andscan.roots/scan.ignore:max-lines(raw, likewc -l) ⇒file-lines-exceeded,max-bytes(raw, fenced/inline code counts) ⇒file-bytes-exceeded, at least one required. Its own module rather than an extension ofstructure, because that one counts the cleaned section text of a Markdown file whilefilecounts the raw file of any kind. Hermetic, opt-in (DC-FA-FILE-001)workflows— declaration consistency of theuses:references of CI workflows below a configured directory (workflows.dir— not hard-wired, because the location is CI-system specific), opt-in. A foreign reference names a full 40-character commit SHA (uses-pin-missing) with a tag comment behind it (uses-pin-untagged) — a tag can be moved, a SHA cannot; the form is checked, not the validity (that would be network). If the same SHA carries more than one tag comment within the scan set — across files — every affected line is reported (uses-pin-tag-conflict); repetition stays clean, and which comment is correct remains network. A local reference (./…) needs no pin, but two other guarantees: the target exists (uses-local-missing), and the calling job carries the permissions the target demands (uses-local-perms-undeclared,uses-local-perms-narrow) — a job without its ownpermissions:inherits the workflow header but cannot pass on what it does not declare. Unparsable YAML is reported (workflow-unparsable) rather than skipped. Hermetic (no git, no network, no execution); references come from the YAML tree, not from a text search (DC-FA-WF-001)reviews— review-report coverage: everydone/slice plan with a review promise needs at least one report under a configured directory (reviews.reviews-dir) — otherwisereview-missing. A promise is a checkbox item matched byreviews.promise-pattern; the default recognizes "unabhängiger Review" and the template form "Review durchgeführt". Reports are matched through theslice-<NNN>identifier in their filename or, withmatch: name, through the slice's base name (for slug identifiers; a report covers only the longest matching name). Withrecursiveandskip-patternthe module reads subdirectories without archived stubs;skip-allows-emptydeclares a fully archiveddone/a resting state. Checked is coverage, not the quality of the report. Hermetic (no git, no network), opt-in (DC-FA-RVW-001)mentions— mention coverage for a set of artifacts: every member of a target set configured through path globs (mentions.artifacts) must appear in at least one document of the likewise configured actual set (mentions.documents) — otherwiseartifact-unmentioned. A different axis than the reference matrix: that one measures traced through identifiers, this one measures mentioned through paths and is therefore schema-free — for artifacts that follow no identifier convention. What is searched for is a standalone mention, not a substring; the target artifacts are never opened. Fail-closed on a missing or empty set (exit 2, not "0 findings"). Hermetic (no git, no network), opt-in (DC-FA-MENT-001)
Every finding names file, line, target and reason; exit codes:
0 clean, 1 findings, 2 environment or configuration error.
Twelve functionally overlapping tool copies from three families
(verify-doc-refs.sh — Shell, check_refs.py — Python,
docs-check.js — JavaScript) grew across the sister repositories of the
development workspace — each with its own feature set, its own drift, its
own maintenance. d-check replaces them with one tool:
- Configuration instead of fork: repo-specific behavior lives
declaratively in
.d-check.yml(DC-FA-CONF-001), not in copied scripts. - Replaceability is measurable: every legacy tool must be replaceable by
d-check with matching configuration — at least the same real findings, no
false positives that break a green CI
(
DC-QA-04). - One distribution path: container image with digest pin instead of
n maintained copies
(
DC-FA-DIST-001).
Documentation is a reference graph with verifiable invariants. Whether a link reaches its target, an anchor exists, an identifier links to its definition, or a document class points downward is machine-decidable — d-check turns these invariants into a gate instead of a review opinion.
The principle: report, never repair. d-check is a purely read-only
tool (DC-QA-03);
deterministic findings are fixed, not suppressed. An opt-out
marker exists only where a non-existent target or an illustrative identifier
can be documented intent (d-check:ignore, per line) — it silences four
modules: codepaths, ids, versions and diagrams. That is a named
list, not a derivable criterion: matrix, structure and citations also
report on lines and do not know the marker. At codepaths and ids the marker counts only outside inline code — in backticks it is a mention, and it must sit inside an HTML comment; at versions and diagrams it stays a raw token — structurally for diagrams, as a named boundary for versions (it does read prose lines)
(DC-FA-CODE-001,
DC-FA-ID-001,
DC-FA-VER-001,
DC-FA-DIAG-001);
all others do not know it.
A checking tool whose own statements wobble is worthless — determinism and side-effect freedom are therefore core contracts of the spec, and both are measured, not asserted:
- Determinism: identical input ⇒ byte-identical output,
stably sorted; tested over ten repeated runs with
hash comparison
(
DC-QA-02). - Side-effect-free and network-free: never writes into the checked
repository, opens no network connections except in the opt-in modules
externalandsources— measured in the gate run with a read-only mount and--network none(DC-QA-03). - No silent defaults: every invalid
.d-check.ymlaborts with exit 2; checking never proceeds with guessed configuration (DC-FA-CONF-001). - Dogfooding: d-check validates its own docs on every gate run — with the self-configuration fully built out (eleven modules incl. reference matrix, span artifacts, host-path hygiene, version-pin consistency, structure invariants, diagram identifiers and verbatim citations).
- Container native-identical: the image's finding output and exit code
are byte-identical to native execution, tested automatically per platform
(
linux/amd64,linux/arm64) (DC-FA-DIST-001); CI consumption via digest pin.
Distributed as a multi-platform container image (linux/amd64, linux/arm64)
via GHCR
(DC-FA-DIST-001),
also mirrored to Docker Hub as pt9912/d-check —
the same image, not a second build, same index digest (one digest pin
works for both registries)
(DC-FA-DIST-002):
docker run --rm -v "$PWD:/repo:ro" ghcr.io/pt9912/d-check:v0.86.1CI pipelines pin to the digest from the release notes rather than to
moving tags. The task-oriented entry point — from the first check to every
single module — is the user handbook (German);
the terse invocation reference with options and exit codes is
docs/user/operations.md, the release and
digest-pin path docs/maintainer/releasing.md.
Without a network, d-check --manual <term> prints the matching sections of
the handbook and the specification from the image itself.
Optional in the repo root; without a file the default modules
links + anchors run over docs/, spec/ and the root *.md:
scan:
roots: ["."] # entire repo root
modules: [links, anchors, ids] # external + sources stay strictly opt-in (network)
ids:
patterns:
- regex: 'ADR-\d{4}'
target: docs/adr/ # identifiers must link hereThe full schema with all keys, defaults and
validation constraints is in the
specification §.d-check.yml;
a living example fully built out (incl. reference matrix) is
this repo's self-configuration. Every invalid
configuration aborts with exit 2 — checking never proceeds with
silent defaults
(DC-FA-CONF-001).
| Document | Contents |
|---|---|
docs/user/benutzerhandbuch.md |
User handbook (German): task-oriented, every rule module with example configuration |
docs/user/operations.md |
Invocation reference: options, exit codes, configuration |
docs/maintainer/releasing.md |
Release process, digest-pin consumption |
spec/lastenheft.md |
Requirements (DC-FA-*, DC-QA-*), acceptance criteria |
harness/README.md |
Harness entry: source precedence, guides, sensors |
AGENTS.md |
Briefing for AI coding agents, hard rules |
docs/plan/planning/in-progress/roadmap.md |
Waves and slices |
CHANGELOG.md |
Change history |
The host needs only git, GNU make, bash and Docker
(see AGENTS.md §3.1).
make help # available targets
make gates # all inner gates (mandatory before handoff)This project is licensed under the MIT License.