DocAnchor.Cli
1.0.0
dotnet tool install --global DocAnchor.Cli --version 1.0.0
dotnet new tool-manifest
dotnet tool install --local DocAnchor.Cli --version 1.0.0
#tool dotnet:?package=DocAnchor.Cli&version=1.0.0
nuke :add-package DocAnchor.Cli --version 1.0.0
DocAnchor
Narrative documentation, assembled from fragments that live where they apply.
A fragment is a small piece of prose with an id. It lives either inside a source comment, right at the line it describes, or in a standalone file. A skeleton is a hand written document that declares the reading order and pulls fragments into named slots. DocAnchor merges the two into plain Markdown that you commit, and it fails the build when a reference points at nothing.
one id space. two homes. one merge. one validator.
Why not an existing tool
Three kinds of tool already exist and each solves one half:
- Snippet embedders pull code into documents, but do not enforce anything and cannot hold prose in the code.
- Traceability tools enforce links between requirements and code, but do not produce a document anybody wants to read.
- Documentation generators render beautifully, and the one that does keep prose in comments ties the reading order to the structure of the code.
DocAnchor is the combination: prose in the code, narrative order outside it, and a hard failure on a broken reference.
Install
dotnet tool install --global DocAnchor.Cli
Optionally add the analyzer to a C# project, so a broken reference is a compiler error with a squiggle in the editor rather than a separate step:
dotnet add package DocAnchor.Analyzers
The analyzer reads the catalog file. Add it to the project that should be checked:
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../docanchor.lock" />
</ItemGroup>
Quick start
docanchor build # merge skeletons, write documents and docanchor.lock
docanchor check # validate and compare against disk, write nothing, for CI
docanchor list # show every fragment, add --orphans for the unused ones
docanchor where <id>
A worked example
A decision that lives in the code, at the line that enforces it:
/* doc: decision/reject-unbalanced-entry
A posting whose legs do not sum to zero is rejected with `UnbalancedEntryException`, and
nothing is written. A partial posting is never persisted.
Reason: a half written posting cannot be reconciled afterwards, so the cheaper failure is
to refuse the whole thing. The audit rule in [[decision/audit-log-is-append-only]] relies
on every stored posting being balanced.
*/
// snip: balance-check
if (total != 0)
{
throw new UnbalancedEntryException(posting.Id);
}
// snip-end
// docref: decision/audit-log-is-append-only
_audit.Add(posting);
A decision with no natural anchor in the code, as a standalone file
docs/fragments/decision/audit-log-is-append-only.md:
---
status: active
---
The audit log is append only. A correction is a new entry that references the entry it
corrects, never an update of the original row.
The skeleton docs/mdsource/01-posting-rules.source.md, which owns the order:
# Posting rules
The ledger treats a posting as one unit: either it is fully valid and persisted, or it is
refused with a reason.
## Balance
## Audit
docanchor build writes docs/01-posting-rules.md with the prose woven in, the code quoted
underneath it, the cross link resolved to the page that carries the target, and a source link back
to the exact line. A complete runnable version of this is in
samples/Example.Ledger.
Markers
| Marker | Where | Meaning |
| --- | --- | --- |
| doc: <id> | source comment or fragment file | defines a fragment, the rest of the comment is its text |
| docref: <id> | source comment | this code depends on that fragment, no prose here |
| snip: <name> … snip-end | source comment | quote the code between the markers verbatim |
| | skeleton | pull the fragment in here | | | skeleton | pull the snippet in here |
| [[<id>]] | fragment text | link to another fragment, resolved to whatever page carries it |
An id is <kind>/<slug>, lowercase, two to four segments. The recommended kinds are decision/
for a deliberate choice, constraint/ for an externally imposed limit, trap/ for a non obvious
failure mode somebody would otherwise "fix", and concept/ for a term the rest of the docs rely
on. The tool does not enforce the set.
Diagnostics
| Id | Meaning | Reported by |
|---|---|---|
| DA0001 | reference to an unknown fragment id | tool and analyzer |
| DA0002 | duplicate fragment id | tool and analyzer |
| DA0003 | fragment defined but no skeleton includes it | tool |
| DA0004 | merged output is stale | tool |
| DA0005 | malformed marker or invalid id syntax | tool and analyzer |
| DA0006 | dangling cross link inside fragment text | tool |
| DA0009 | catalog missing, so ids cannot be checked | analyzer |
Exit codes: 0 clean, 1 defects found, 2 usage or internal error.
What the analyzer cannot do
The analyzer sees one compilation at a time and never sees the skeletons, the standalone fragment
files or the other host languages. It therefore cannot report DA0003, DA0004 or DA0006. Those stay
in docanchor check.
Run both in continuous integration. A green build is not by itself evidence that the documentation is complete.
DA0001, DA0002 and DA0005 default to error severity, so they fail a build without needing
TreatWarningsAsErrors. Lower them in .editorconfig if you would rather start with warnings:
dotnet_diagnostic.DA0001.severity = warning
DA0002 is reported at compilation end, which means it appears on build but not always live in the editor. DA0001 and DA0005 appear live.
Configuration
docanchor.json in the repository root. Every field has a default.
{
"skeletonRoot": "docs/mdsource",
"outputRoot": "docs",
"fragmentRoot": "docs/fragments",
"scan": ["src/**/*.cs", "build/**/*.ps1", "db/**/*.sql"],
"exclude": ["**/bin/**", "**/obj/**"]
}
C# is read through Roslyn, so verbatim strings, raw string literals and interpolations never produce a false marker. PowerShell, SQL and Markdown are read by a scanner that knows their string literals, here strings and fenced blocks. The skeleton root, output root and fragment root are never scanned for definitions, whatever the patterns say.
Continuous integration
docanchor check
That single command validates every reference, every skeleton and every merged document, and returns a non zero exit code with a report naming file, line, id and remedy.
Conventions that make it work
The tool cannot enforce these, and ignoring them produces a system that is more work than plain prose:
- A fragment never knows its neighbours. No "as described above". A fragment must be readable on its own, because it may appear on several pages.
- Order lives only in the skeleton. Reordering a chapter means editing one file and never touching code.
- One fact, one place, never restated. A skeleton may weave and connect, but must not repeat a fragment's content in its own words. The moment a fact exists twice in prose, the system is worse than no system.
- A fragment is a paragraph to half a page. Smaller and you administer confetti, larger and it is a chapter that belongs in a skeleton.
- Deleting is not an option, moving is. When code that carried a decision goes away, move the fragment to a standalone file or mark it superseded. DA0003 exists to force that choice.
Non goals
No renderer and no site generator, Markdown out is the contract. No API documentation generation. No requirements process, approval workflow or traceability matrices. No text generation. No PDF, localisation or diagrams. No watch mode. No host languages beyond C#, PowerShell, SQL and Markdown.
Version semantics, that is detecting "the decision changed and the code did not follow", is specified but not built. It is the most valuable future feature and deliberately waits until the id convention has settled in real use.
Its own documentation
DocAnchor documents itself with DocAnchor. See docs for the merged result and
docs/mdsource for the skeletons that produce it. docanchor check runs in its own build.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 19 | 8/20/2026 |