github workflows

This commit is contained in:
Denis Angell
2026-05-13 18:21:39 +02:00
parent a761b0d43c
commit 536f87b952
9 changed files with 1358 additions and 2 deletions

29
.github/doc-coverage-thresholds.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"global_minimum": 0,
"ratchet_mode": "no_decrease",
"new_file_minimum": 80,
"module_thresholds": {
"include/xrpl/basics/": 0,
"include/xrpl/crypto/": 0,
"include/xrpl/protocol/": 0,
"include/xrpl/ledger/": 0,
"include/xrpl/tx/": 0,
"include/xrpl/server/": 0,
"include/xrpl/nodestore/": 0,
"include/xrpl/shamap/": 0,
"include/xrpl/resource/": 0,
"src/xrpld/rpc/": 0,
"src/xrpld/overlay/": 0,
"src/xrpld/peerfinder/": 0,
"src/xrpld/consensus/": 0,
"src/xrpld/app/": 0,
"src/libxrpl/": 0
},
"schedule": {
"2026-Q3": { "global_minimum": 30 },
"2026-Q4": { "global_minimum": 40 },
"2027-Q1": { "global_minimum": 50 },
"2027-Q2": { "global_minimum": 60 },
"2027-Q3": { "global_minimum": 70 }
}
}

277
.github/scripts/doc-coverage-check.py vendored Normal file
View File

@@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""
Documentation coverage checker for xrpld.
Parses coverxygen LCOV output, compares against per-module thresholds
defined in .github/doc-coverage-thresholds.json, and generates a
markdown report suitable for posting as a PR comment.
Usage:
python3 doc-coverage-check.py \
--lcov-file doc-coverage.info \
--threshold-file .github/doc-coverage-thresholds.json \
--output doc-coverage-report.md \
[--base-lcov-file base-doc-coverage.info]
"""
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
def parse_lcov(lcov_path: str) -> dict[str, dict[str, int]]:
"""Parse LCOV-format file into per-file coverage data.
Returns a dict mapping file paths to {"documented": N, "total": N}.
"""
coverage = {}
current_file = None
documented = 0
total = 0
with open(lcov_path) as f:
for line in f:
line = line.strip()
if line.startswith("SF:"):
current_file = line[3:]
documented = 0
total = 0
elif line.startswith("DA:"):
parts = line[3:].split(",")
if len(parts) >= 2:
total += 1
if int(parts[1]) > 0:
documented += 1
elif line == "end_of_record":
if current_file:
coverage[current_file] = {
"documented": documented,
"total": total,
}
current_file = None
return coverage
def compute_module_coverage(
coverage: dict[str, dict[str, int]],
module_prefixes: list[str],
) -> dict[str, dict[str, int | float]]:
"""Aggregate file-level coverage into module-level stats."""
modules = {}
for prefix in module_prefixes:
doc = 0
tot = 0
for filepath, stats in coverage.items():
if filepath.startswith(prefix) or f"/{prefix}" in filepath:
doc += stats["documented"]
tot += stats["total"]
pct = (doc / tot * 100) if tot > 0 else 0.0
modules[prefix] = {"documented": doc, "total": tot, "percent": round(pct, 1)}
return modules
def compute_global_coverage(
coverage: dict[str, dict[str, int]],
) -> dict[str, int | float]:
"""Compute overall coverage across all files."""
doc = sum(s["documented"] for s in coverage.values())
tot = sum(s["total"] for s in coverage.values())
pct = (doc / tot * 100) if tot > 0 else 0.0
return {"documented": doc, "total": tot, "percent": round(pct, 1)}
def check_ratchet(
current: dict[str, dict[str, int | float]],
base: dict[str, dict[str, int | float]] | None,
current_global: dict[str, int | float],
base_global: dict[str, int | float] | None,
) -> list[str]:
"""Check that no module or global coverage decreased vs base branch."""
violations = []
if base_global and current_global["percent"] < base_global["percent"]:
violations.append(
f"Global coverage decreased: {base_global['percent']}% -> "
f"{current_global['percent']}%"
)
if base:
for module, stats in current.items():
if module in base and stats["percent"] < base[module]["percent"]:
violations.append(
f"`{module}` coverage decreased: "
f"{base[module]['percent']}% -> {stats['percent']}%"
)
return violations
def check_new_files(
coverage: dict[str, dict[str, int]],
new_files: list[str],
min_coverage: int,
) -> list[str]:
"""Check that new files meet minimum documentation coverage."""
violations = []
for filepath in new_files:
for covered_path, stats in coverage.items():
if filepath in covered_path or covered_path.endswith(filepath):
if stats["total"] > 0:
pct = stats["documented"] / stats["total"] * 100
if pct < min_coverage:
violations.append(
f"`{filepath}` has {pct:.0f}% doc coverage "
f"(minimum {min_coverage}%)"
)
break
return violations
def coverage_emoji(pct: float) -> str:
if pct >= 80:
return "+"
if pct >= 50:
return "~"
return "-"
def generate_report(
global_stats: dict[str, int | float],
module_stats: dict[str, dict[str, int | float]],
thresholds: dict,
violations: list[str],
new_file_violations: list[str],
) -> str:
"""Generate a markdown report for the PR comment."""
lines = []
lines.append("## Documentation Coverage Report")
lines.append("")
passed = not violations and not new_file_violations
status = "PASSED" if passed else "FAILED"
lines.append(f"**Status:** {status}")
lines.append(
f"**Global Coverage:** {global_stats['percent']}% "
f"({global_stats['documented']}/{global_stats['total']} entities documented)"
)
lines.append(
f"**Minimum Threshold:** {thresholds.get('global_minimum', 0)}%"
)
lines.append("")
if violations or new_file_violations:
lines.append("### Violations")
lines.append("")
for v in violations + new_file_violations:
lines.append(f"- {v}")
lines.append("")
lines.append("### Module Coverage")
lines.append("")
lines.append("| Module | Coverage | Documented | Total | Threshold |")
lines.append("|--------|----------|------------|-------|-----------|")
module_thresholds = thresholds.get("module_thresholds", {})
for module in sorted(module_stats.keys()):
stats = module_stats[module]
threshold = module_thresholds.get(module, 0)
emoji = coverage_emoji(stats["percent"])
lines.append(
f"| `{module}` | {stats['percent']}% | "
f"{stats['documented']} | {stats['total']} | {threshold}% |"
)
lines.append("")
lines.append(
"*Coverage measured by [coverxygen](https://github.com/psycofdj/coverxygen). "
"See [docs/DOCUMENTATION_STANDARDS.md](../docs/DOCUMENTATION_STANDARDS.md) "
"for documentation guidelines.*"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Check documentation coverage")
parser.add_argument("--lcov-file", required=True, help="Path to LCOV coverage file")
parser.add_argument(
"--threshold-file", required=True, help="Path to thresholds JSON"
)
parser.add_argument("--output", required=True, help="Path to write markdown report")
parser.add_argument(
"--base-lcov-file", default=None, help="Path to base branch LCOV file"
)
parser.add_argument(
"--new-files",
default="",
help="Comma-separated list of new C++ files in this PR",
)
args = parser.parse_args()
with open(args.threshold_file) as f:
thresholds = json.load(f)
coverage = parse_lcov(args.lcov_file)
module_prefixes = list(thresholds.get("module_thresholds", {}).keys())
module_stats = compute_module_coverage(coverage, module_prefixes)
global_stats = compute_global_coverage(coverage)
base_coverage = None
base_module_stats = None
base_global_stats = None
if args.base_lcov_file and Path(args.base_lcov_file).exists():
base_coverage = parse_lcov(args.base_lcov_file)
base_module_stats = compute_module_coverage(base_coverage, module_prefixes)
base_global_stats = compute_global_coverage(base_coverage)
violations = []
if global_stats["percent"] < thresholds.get("global_minimum", 0):
violations.append(
f"Global coverage {global_stats['percent']}% is below minimum "
f"{thresholds['global_minimum']}%"
)
for module, threshold in thresholds.get("module_thresholds", {}).items():
if module in module_stats and module_stats[module]["percent"] < threshold:
violations.append(
f"`{module}` coverage {module_stats[module]['percent']}% is below "
f"threshold {threshold}%"
)
if thresholds.get("ratchet_mode") == "no_decrease":
violations.extend(
check_ratchet(
module_stats, base_module_stats, global_stats, base_global_stats
)
)
new_file_violations = []
if args.new_files:
new_files = [f.strip() for f in args.new_files.split(",") if f.strip()]
new_file_min = thresholds.get("new_file_minimum", 80)
new_file_violations = check_new_files(coverage, new_files, new_file_min)
report = generate_report(
global_stats, module_stats, thresholds, violations, new_file_violations
)
with open(args.output, "w") as f:
f.write(report)
print(report)
if violations or new_file_violations:
print(f"\nFAILED: {len(violations) + len(new_file_violations)} violation(s)")
sys.exit(1)
else:
print("\nPASSED: All coverage thresholds met")
sys.exit(0)
if __name__ == "__main__":
main()

279
.github/scripts/doc-review.py vendored Normal file
View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""
Diff-aware documentation review for xrpld PRs.
For each changed C++ file, extracts the diff hunks and existing doc
comments, then asks the Anthropic API whether documentation needs
updating. Produces:
- doc-review-report.md: summary comment for the PR
- doc-review-comments.json: inline review comments with file/line info
"""
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
try:
import anthropic
except ImportError:
print("ERROR: anthropic package not installed. Run: pip install anthropic")
sys.exit(1)
MODEL = "claude-sonnet-4-6"
MAX_TOKENS = 2048
SYSTEM_PROMPT = """You are a documentation reviewer for the xrpld (XRP Ledger daemon) C++ codebase.
Your job is to review code changes and determine whether existing documentation
comments need updating, or whether new documentation is needed.
Documentation style: Javadoc-style Doxygen comments (/** ... */).
See the project's docs/DOCUMENTATION_STANDARDS.md for full guidelines.
Rules:
- Only flag REAL semantic drift: changed behavior, new parameters, removed
functionality, changed return values, new error conditions.
- Do NOT flag cosmetic changes (whitespace, formatting, variable renames that
don't change semantics).
- Do NOT suggest docs for private implementation details unless the logic is
genuinely non-obvious.
- Do NOT paraphrase function signatures. Good docs explain WHY and WHAT
BEHAVIOR, not WHAT THE CODE LITERALLY DOES.
- Be terse. Each finding should be 1-3 sentences.
For each issue found, respond with a JSON array of objects:
{
"issues": [
{
"file": "path/to/file.h",
"line": 42,
"severity": "warning" | "suggestion",
"message": "Brief description of the doc issue",
"suggested_doc": "Optional: suggested doc comment text"
}
],
"summary": "One-paragraph summary of documentation state for this file"
}
If no issues are found, return: {"issues": [], "summary": "Documentation is up to date."}
Respond ONLY with valid JSON. No markdown fences, no explanation outside JSON."""
@dataclass
class FileAnalysis:
path: str
diff: str
existing_docs: str
file_content: str
def get_diff(base_sha: str, head_sha: str, filepath: str) -> str:
"""Get the unified diff for a specific file between two commits."""
try:
result = subprocess.run(
["git", "diff", f"{base_sha}...{head_sha}", "--", filepath],
capture_output=True,
text=True,
check=True,
)
return result.stdout
except subprocess.CalledProcessError:
return ""
def extract_doc_comments(content: str) -> str:
"""Extract all /** ... */ doc comments from file content."""
pattern = r'/\*\*[\s\S]*?\*/'
matches = re.findall(pattern, content)
return "\n\n".join(matches) if matches else "(no documentation comments found)"
def read_file_safe(filepath: str) -> str:
"""Read a file, returning empty string if it doesn't exist."""
try:
return Path(filepath).read_text(encoding="utf-8", errors="replace")
except (FileNotFoundError, PermissionError):
return ""
def analyze_file(client: anthropic.Anthropic, analysis: FileAnalysis) -> dict:
"""Send a file's diff and docs to the API for review."""
user_prompt = f"""Review the following code change for documentation accuracy.
## File: {analysis.path}
## Git Diff:
```
{analysis.diff[:8000]}
```
## Existing Documentation Comments:
```
{analysis.existing_docs[:4000]}
```
## Current File Content (first 200 lines for context):
```cpp
{chr(10).join(analysis.file_content.split(chr(10))[:200])}
```
Analyze whether the diff introduces changes that make existing docs inaccurate,
or adds new public API surface that lacks documentation."""
try:
response = client.messages.create(
model=MODEL,
max_tokens=MAX_TOKENS,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_prompt}],
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = re.sub(r'^```\w*\n?', '', text)
text = re.sub(r'\n?```$', '', text)
return json.loads(text)
except (json.JSONDecodeError, Exception) as e:
return {
"issues": [],
"summary": f"Analysis failed: {str(e)[:200]}",
}
def generate_report(
results: dict[str, dict],
changed_files: list[str],
) -> str:
"""Generate the markdown summary report."""
lines = ["## Documentation Review Report", ""]
total_issues = sum(len(r.get("issues", [])) for r in results.values())
warnings = sum(
1
for r in results.values()
for i in r.get("issues", [])
if i.get("severity") == "warning"
)
suggestions = total_issues - warnings
if total_issues == 0:
lines.append("No documentation issues found.")
else:
lines.append(
f"Found **{total_issues}** documentation issue(s) "
f"across **{len(changed_files)}** changed file(s): "
f"{warnings} warning(s), {suggestions} suggestion(s)."
)
lines.append("")
lines.append(f"Files reviewed: {len(changed_files)}")
lines.append("")
for filepath, result in sorted(results.items()):
issues = result.get("issues", [])
summary = result.get("summary", "")
if issues:
lines.append(f"### `{filepath}`")
lines.append("")
lines.append(summary)
lines.append("")
for issue in issues:
severity = issue.get("severity", "suggestion")
icon = "**Warning:**" if severity == "warning" else "**Suggestion:**"
line_num = issue.get("line", "?")
msg = issue.get("message", "")
lines.append(f"- {icon} Line {line_num}: {msg}")
lines.append("")
lines.append("---")
lines.append(
"*Automated documentation review. "
"See [docs/DOCUMENTATION_STANDARDS.md](../docs/DOCUMENTATION_STANDARDS.md) "
"for guidelines.*"
)
return "\n".join(lines)
def generate_inline_comments(results: dict[str, dict]) -> list[dict]:
"""Generate inline PR review comments from analysis results."""
comments = []
for filepath, result in results.items():
for issue in result.get("issues", []):
line = issue.get("line")
if not line or not isinstance(line, int):
continue
body = issue.get("message", "")
suggested = issue.get("suggested_doc")
if suggested:
body += f"\n\n**Suggested documentation:**\n```cpp\n{suggested}\n```"
severity = issue.get("severity", "suggestion")
prefix = "Doc Warning" if severity == "warning" else "Doc Suggestion"
body = f"**{prefix}:** {body}"
comments.append({"path": filepath, "line": line, "body": body})
return comments
def main():
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
print("ERROR: ANTHROPIC_API_KEY not set")
sys.exit(1)
changed_files_str = os.environ.get("CHANGED_FILES", "")
if not changed_files_str:
print("No changed files to review")
sys.exit(0)
base_sha = os.environ.get("BASE_SHA", "HEAD~1")
head_sha = os.environ.get("HEAD_SHA", "HEAD")
changed_files = [f.strip() for f in changed_files_str.split() if f.strip()]
cpp_files = [
f for f in changed_files if f.endswith((".h", ".hpp", ".cpp"))
]
if not cpp_files:
print("No C++ files changed")
sys.exit(0)
print(f"Reviewing {len(cpp_files)} file(s) for documentation accuracy...")
client = anthropic.Anthropic(api_key=api_key)
results = {}
for filepath in cpp_files:
print(f" Analyzing: {filepath}")
diff = get_diff(base_sha, head_sha, filepath)
if not diff:
continue
content = read_file_safe(filepath)
existing_docs = extract_doc_comments(content)
analysis = FileAnalysis(
path=filepath,
diff=diff,
existing_docs=existing_docs,
file_content=content,
)
results[filepath] = analyze_file(client, analysis)
report = generate_report(results, cpp_files)
Path("doc-review-report.md").write_text(report)
print("\nReport written to doc-review-report.md")
comments = generate_inline_comments(results)
Path("doc-review-comments.json").write_text(json.dumps(comments, indent=2))
print(f"Generated {len(comments)} inline comment(s)")
if __name__ == "__main__":
main()

109
.github/workflows/doc-coverage.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Documentation Coverage
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'include/**'
- 'src/libxrpl/**'
- 'src/xrpld/**'
- 'docs/Doxyfile'
- '.github/doc-coverage-thresholds.json'
- '.github/workflows/doc-coverage.yml'
concurrency:
group: doc-coverage-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
coverage:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/tools-rippled-documentation:sha-a8c7be1
steps:
- name: Checkout PR branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install coverxygen
run: pip install coverxygen
- name: Determine new C++ files
id: new-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
include/**/*.h
src/**/*.h
src/**/*.cpp
since_last_remote_commit: false
- name: Build Doxygen XML (PR branch)
env:
BUILD_DIR: build-pr
run: |
mkdir -p "${BUILD_DIR}"
cd "${BUILD_DIR}"
cmake -Donly_docs=ON ..
cmake --build . --target docs
- name: Generate coverage report (PR branch)
run: |
coverxygen \
--xml-dir build-pr/docs/xml \
--src-dir . \
--output doc-coverage.info \
--kind class,struct,function,enum,typedef,variable \
--scope public
- name: Build Doxygen XML (base branch)
env:
BUILD_DIR: build-base
run: |
git checkout ${{ github.event.pull_request.base.sha }}
mkdir -p "${BUILD_DIR}"
cd "${BUILD_DIR}"
cmake -Donly_docs=ON ..
cmake --build . --target docs || true
git checkout ${{ github.event.pull_request.head.sha }}
- name: Generate coverage report (base branch)
run: |
if [ -d "build-base/docs/xml" ]; then
coverxygen \
--xml-dir build-base/docs/xml \
--src-dir . \
--output base-doc-coverage.info \
--kind class,struct,function,enum,typedef,variable \
--scope public || true
fi
- name: Check coverage thresholds
run: |
BASE_FLAG=""
if [ -f "base-doc-coverage.info" ]; then
BASE_FLAG="--base-lcov-file base-doc-coverage.info"
fi
NEW_FILES=""
if [ -n "${{ steps.new-files.outputs.added_files }}" ]; then
NEW_FILES="--new-files ${{ steps.new-files.outputs.added_files }}"
fi
python3 .github/scripts/doc-coverage-check.py \
--lcov-file doc-coverage.info \
--threshold-file .github/doc-coverage-thresholds.json \
--output doc-coverage-report.md \
${BASE_FLAG} \
${NEW_FILES} || true
- name: Post coverage report to PR
if: always()
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
with:
header: doc-coverage
path: doc-coverage-report.md

98
.github/workflows/doc-review.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Documentation Review
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'include/**/*.h'
- 'src/libxrpl/**/*.h'
- 'src/libxrpl/**/*.cpp'
- 'src/xrpld/**/*.h'
- 'src/xrpld/**/*.cpp'
concurrency:
group: doc-review-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
- name: Install dependencies
run: pip install anthropic
- name: Determine changed C++ files
id: changes
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
include/**/*.h
src/libxrpl/**/*.h
src/libxrpl/**/*.cpp
src/xrpld/**/*.h
src/xrpld/**/*.cpp
- name: Run documentation review
if: steps.changes.outputs.any_changed == 'true'
env:
CHANGED_FILES: ${{ steps.changes.outputs.all_changed_files }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 .github/scripts/doc-review.py
- name: Post review summary
if: steps.changes.outputs.any_changed == 'true' && always()
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
with:
header: doc-review
path: doc-review-report.md
- name: Post inline review comments
if: steps.changes.outputs.any_changed == 'true' && always()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const fs = require('fs');
if (!fs.existsSync('doc-review-comments.json')) return;
const comments = JSON.parse(fs.readFileSync('doc-review-comments.json', 'utf8'));
if (comments.length === 0) return;
const pull_number = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
for (const comment of comments) {
try {
await github.rest.pulls.createReviewComment({
owner,
repo,
pull_number,
body: comment.body,
commit_id: '${{ github.event.pull_request.head.sha }}',
path: comment.path,
line: comment.line,
side: 'RIGHT',
});
} catch (e) {
console.log(`Failed to post comment on ${comment.path}:${comment.line}: ${e.message}`);
}
}

345
SCOPE_OF_WORK.md Normal file
View File

@@ -0,0 +1,345 @@
# XRPLD Automated Documentation System — Scope of Work
## 1. Problem Statement
The XRP Ledger daemon (`xrpld`) is a ~275,000 line C++ codebase with 1,183
source files across the core library, protocol layer, and application server.
It is the single implementation of the XRP Ledger protocol and processes
billions of dollars in value.
Despite this criticality, the codebase has minimal inline documentation. Only
569 of 1,183 files contain any Doxygen-style doc comments, and most of those
are sparse — a class-level sentence or two, rarely covering individual methods,
parameters, or behavioral invariants.
The only formal documentation effort — an external specification by Common
Prefix — has fundamental structural problems:
- **Drift is the default state.** The spec lives in a separate repository
with no CI linkage to the codebase. Every commit to `rippled` that changes
behavior silently invalidates the spec. Even one week of drift makes
the spec unreliable.
- **Separate repo, separate context.** No contributor has both repos open.
When a bug comes in, the developer reads the code, not the spec. A
recent bug would have been caught if the code itself was documented.
- **No code-level documentation.** The spec describes system-level behavior
(payment engine, DEX) but does not document individual functions, classes,
parameters, or invariants. A developer working on a specific function
gets no help.
- **Vendor dependency.** Ripple has a critical documentation dependency on a
single external firm. If the contract ends, the spec orphans.
- **Perverse incentive.** The vendor profits from complexity and drift.
Cleaner code and better inline docs reduce the need for external
specification work.
## 2. Proposed Solution
Build an automated, in-repo documentation system with four components:
1. **Initial documentation pass** — Comprehensively document all 1,183
source files using Claude Code with deep xrpld context
2. **Continuous maintenance** — A GitHub Action on every PR that detects
doc drift and suggests updates, using diff-aware LLM analysis
3. **Coverage enforcement** — CI-enforced documentation coverage thresholds
that ratchet up over time, preventing regression
4. **Developer agents** — Claude Code commands for onboarding, architecture
questions, doc review, and bug pattern detection
All documentation lives alongside the code. No external repos. No external
dependencies. Documentation accuracy is enforced by CI the same way code
style and test coverage are enforced today.
## 3. Deliverables
### 3.1 Documentation Standards (docs/DOCUMENTATION_STANDARDS.md)
A canonical format guide defining:
- Javadoc-style `/** ... */` Doxygen comments (matches 5,718 existing
instances in the codebase)
- Documentation levels: file, class, public method, free function, enum
- Required Doxygen tags: `@param`, `@return`, `@note`, `@invariant`
- Quality rules: document behavior and invariants, never paraphrase
signatures, terse style (2-5 lines for classes, 1-3 for functions)
**Status: Complete.** File created at `docs/DOCUMENTATION_STANDARDS.md`.
### 3.2 Doxygen Configuration Changes (docs/Doxyfile)
- `EXTRACT_ALL = NO` (was `YES`) — so undocumented entities are flagged
rather than silently extracted
- `GENERATE_XML = YES` (was `NO`) — required for coverxygen to parse
and measure documentation coverage
**Status: Complete.** Changes applied to `docs/Doxyfile`.
### 3.3 Documentation Coverage Pipeline
**Components:**
| File | Purpose |
|------|---------|
| `.github/doc-coverage-thresholds.json` | Per-module thresholds + quarterly ratchet schedule |
| `.github/scripts/doc-coverage-check.py` | Parses coverxygen LCOV output, checks thresholds, generates PR report |
| `.github/workflows/doc-coverage.yml` | CI workflow: builds Doxygen XML, runs coverxygen, posts coverage to PR |
| `cmake/XrplDocs.cmake` | New `docs-coverage` CMake target |
**How it works:**
1. On every PR touching C++ files, the workflow builds Doxygen XML output
for both the PR branch and the base branch
2. Coverxygen generates LCOV-format coverage reports from the XML
3. The check script compares coverage against per-module thresholds
4. Ratchet mode (`no_decrease`) prevents any PR from reducing doc coverage
5. New files added in a PR require >= 80% doc coverage
6. Results are posted as a sticky PR comment with per-module breakdown
**Status: Complete.** All files created.
### 3.4 Doc Review GitHub Action
**Components:**
| File | Purpose |
|------|---------|
| `.github/scripts/doc-review.py` | Diff-aware LLM analysis script |
| `.github/workflows/doc-review.yml` | CI workflow: runs on PR, posts review |
**How it works:**
1. On every PR, determines which C++ files changed
2. For each changed file, extracts the git diff hunks and existing doc
comments
3. Sends both to the Anthropic API with a prompt tuned for xrpld: "Given
this diff, are existing docs still accurate?"
4. Posts results as **inline review comments** on specific lines AND a
**summary comment** on the PR
5. Starts in **warning-only mode** (does not block merge)
**Cost control:** Only processes changed files and changed hunks within
those files. A typical PR touches 3-10 files. Estimated cost: $0.05-0.15
per PR.
**Status: Complete.** All files created.
### 3.5 Claude Code Agent Commands
Four developer-facing commands in `.claude/commands/`:
| Command | Purpose |
|---------|---------|
| `doc-review` | Review doc accuracy for files changed on current branch |
| `explain-module` | Explain a module's architecture, classes, control flow, and entry points |
| `how-does-x-work` | Trace a feature through the codebase with file/line references |
| `find-bug-patterns` | Scan code for common xrpld bug patterns (unchecked TER, integer overflow, missing amendment gates, etc.) |
**Status: Complete.** All files created.
### 3.6 Full Codebase Documentation
The initial documentation pass covers 1,183 C++ files organized into 21
module-level PRs. Each PR is scoped to a single subsystem so one domain
expert can review it.
**Status: Not started.** This is the primary execution phase (see Section 5).
## 4. Resources Required
### 4.1 People
| Role | Responsibility | Estimated Time |
|------|---------------|----------------|
| **Documentation lead** (1 person) | Runs Claude Code for each module, reviews output quality, submits PRs, iterates on prompt quality | 50-60% for 15 weeks |
| **Domain reviewers** (3-5 people, rotating) | Review doc PRs for semantic accuracy in their area of expertise. Each reviewer handles 3-5 PRs. | 2-4 hours per PR |
| **CI/infrastructure** (1 person) | Deploys workflows, monitors costs, tunes false positive rate on doc-review action | 10-15% for 15 weeks |
**Total estimated effort:** ~1 FTE for 15 weeks + ~80-120 hours of
reviewer time spread across 3-5 engineers.
### 4.2 Infrastructure & Tools
| Resource | Purpose | Cost |
|----------|---------|------|
| **Anthropic API access** | Powers doc-review GitHub Action | ~$50-100/month (20-30 PRs/week, ~2K tokens per file analysis) |
| **Claude Code license** | Initial documentation pass + developer agent commands | Existing license |
| **GitHub Actions minutes** | Doc-coverage workflow (Doxygen XML build + coverxygen) | ~5-10 min per PR on existing `ubuntu-latest` runners |
| **Coverxygen** | Python package, open source (MIT) | Free |
| **Doxygen** | Already configured and used — existing `ghcr.io/xrplf/ci/tools-rippled-documentation` container | Free (already in CI) |
| **GitHub Actions secret** | `ANTHROPIC_API_KEY` — needed for doc-review workflow | N/A |
**Estimated ongoing cost after initial pass:** $50-150/month for API
usage, negligible CI compute on existing runners.
### 4.3 Access & Permissions
- Write access to the `rippled` repository (or a fork for initial PRs)
- Ability to add GitHub Actions secrets (`ANTHROPIC_API_KEY`)
- Ability to modify required status checks (when promoting doc-review
from warning to required)
## 5. Execution Plan
### Phase 0: Infrastructure — Week 1
Ship the tooling as a single foundational PR:
- [x] `docs/DOCUMENTATION_STANDARDS.md`
- [x] `docs/Doxyfile` modifications
- [x] `.github/doc-coverage-thresholds.json`
- [x] `.github/scripts/doc-coverage-check.py`
- [x] `.github/workflows/doc-coverage.yml`
- [x] `cmake/XrplDocs.cmake` modifications
- [x] `.github/workflows/doc-review.yml`
- [x] `.github/scripts/doc-review.py`
- [x] `.claude/commands/` (4 agent commands)
**Exit criteria:** All workflows pass on a test PR. Coverage report
renders correctly. Doc-review action posts comments without false positives
on a sample PR.
### Phase 1: Foundation Modules — Weeks 2-4
Document the lowest-level modules first (everything else depends on these):
| PR | Module | ~Files | ~Lines |
|----|--------|--------|--------|
| 1 | `include/xrpl/basics/` + `src/libxrpl/basics/` | 63 | ~15K |
| 2 | `include/xrpl/crypto/` + `src/libxrpl/crypto/` | 6 | ~1.5K |
| 3 | `include/xrpl/json/` + `src/libxrpl/json/` | 18 | ~4K |
| 4 | `include/xrpl/beast/` + `src/libxrpl/beast/` | 88 | ~20K |
**Process per PR:**
1. Create branch `docs/module-<name>` from `develop`
2. Run Claude Code against each file with full context: the file itself,
its includes, corresponding test files, and the module README
3. Generate `/** */` doc comments following DOCUMENTATION_STANDARDS.md
4. Domain expert reviews for semantic accuracy
5. Run Doxygen build to validate no doc errors
6. Merge, ratchet that module's threshold up to actual coverage level
**Exit criteria:** 4 PRs merged. Coverage for these modules at 60%+.
Doc-review action running in warning mode on all subsequent PRs.
### Phase 2: Protocol & Transaction Engine — Weeks 4-8
| PR | Module | ~Files |
|----|--------|--------|
| 5 | `include/xrpl/protocol/` + `src/libxrpl/protocol/` | 150 |
| 6 | `include/xrpl/ledger/` + `src/libxrpl/ledger/` | 68 |
| 7 | `include/xrpl/conditions/` + `src/libxrpl/conditions/` | 8 |
| 8 | `include/xrpl/tx/` (core framework: Transactor, ApplyContext) | 15 |
| 9 | Payment transactors | 9 |
| 10 | DEX/AMM transactors | 25 |
| 11 | Escrow transactors | 7 |
| 12 | Other transactors (NFT, token, vault, check, etc.) | 60 |
| 13 | Pathfinding + invariants | 30 |
**Exit criteria:** 9 PRs merged. Global coverage at 40%+. Doc-review
false positive rate tracked and < 10%.
### Phase 3: Server & Application Layer — Weeks 8-13
| PR | Module | ~Files |
|----|--------|--------|
| 14 | `include/xrpl/server/` + `src/libxrpl/server/` | 35 |
| 15 | `include/xrpl/nodestore/` + `src/libxrpl/nodestore/` | 30 |
| 16 | SHAMap | 25 |
| 17 | Resource management | 17 |
| 18 | Overlay + peerfinder | 56 |
| 19 | Consensus | 15 |
| 20 | Application core (ledger, main, misc, rdb) | 133 |
| 21 | RPC handlers | 131 |
**Exit criteria:** 8 PRs merged. Global coverage at 60%+. Doc-review
action promoted from warning to **required check**.
### Phase 4: Tests & Polish — Weeks 13-15
- Document test files (brief docs only test name + what it validates)
- Global threshold at 70%
- Full coverage trend reporting on GitHub Pages
- Retrospective: review false positive rate, API costs, contributor
feedback
**Exit criteria:** 70% global doc coverage. Doc-review required check
with < 5% false positive rate. Coverage trend visible on GitHub Pages.
## 6. Threshold Ratchet Schedule
Coverage thresholds increase quarterly to prevent regression and drive
gradual improvement:
| Quarter | Global Minimum | Enforcement |
|---------|---------------|-------------|
| Launch (2026-Q2) | 0% | `no_decrease` ratchet only |
| 2026-Q3 | 30% | Blocks PRs below threshold |
| 2026-Q4 | 40% | |
| 2027-Q1 | 50% | |
| 2027-Q2 | 60% | |
| 2027-Q3 | 70% | Target steady state |
New files always require 80% coverage regardless of the global threshold.
## 7. Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| LLM generates plausible but wrong docs | Medium | High | Every doc PR requires human domain expert review. Model output is a draft, not final product. |
| Doc-review action false positives annoy contributors | Medium | Medium | Warning-only mode for 3 months. Promote to required only when FP rate < 5%. |
| Coverage enforcement blocks unrelated PRs | Low | Medium | Start at 0% threshold with `no_decrease` only. Quarterly increases announced in advance. |
| Reviewer bandwidth bottleneck | Medium | Medium | PRs scoped to single modules. Reviewers rotate. 2-4 hours per PR is manageable. |
| API costs exceed budget | Low | Low | Only processes diff hunks, not full files. ~$0.05-0.15/PR. Monthly budget cap of $200 with alerting. |
| Doxygen XML build adds CI time | Low | Low | Runs in parallel with existing checks. Uses existing documentation container. ~5 min. |
| Doc comments add code noise | Low | Low | Terse style enforced by standards. 2-5 lines per class, 1-3 per function. |
| Initial pass takes longer than 15 weeks | Medium | Low | Modules are independent. Can parallelize with multiple contributors. Lower-priority modules can slip. |
## 8. Success Metrics
| Metric | Target | Measurement |
|--------|--------|-------------|
| Documentation coverage (public API) | 70% | Coverxygen LCOV reports in CI |
| Doc drift catch rate | > 90% of behavioral changes flagged | Sample audit of merged PRs vs doc-review output |
| False positive rate (doc-review action) | < 5% | Track dismissed vs accepted suggestions |
| Zero spec-vs-code contradictions | 0 incidents | Bug reports citing wrong documentation |
| Contributor satisfaction | > 4/5 rating | Quarterly survey: "docs helped me understand the code" |
| Onboarding time reduction | 30% faster first meaningful PR | Measure across new contributors before/after |
| API cost | < $150/month steady state | Anthropic API billing dashboard |
## 9. What This Replaces
This system does **not** replace the Common Prefix formal verification
work directly — formal verification and code documentation solve different
problems. However, it eliminates the need for an external specification as
the "source of truth" for how xrpld behaves:
| Need | Before | After |
|------|--------|-------|
| "What does this function do?" | Read the code, guess | Read the inline doc |
| "How does the payment engine work?" | Read Common Prefix spec (maybe stale) | Run `/explain-module` or `/how-does-x-work` |
| "Did this PR break any documented behavior?" | Manual review, hope someone notices | Doc-review action flags it automatically |
| "What's our documentation coverage?" | Unknown | Measured per-module in every PR |
| "Is the spec up to date?" | Check manually, probably not | Docs are in-repo, enforced by CI |
## 10. Out of Scope
- **Formal verification.** This project documents code behavior; it does
not prove correctness. Formal verification is a separate discipline.
- **External-facing API documentation.** This covers the C++ source code,
not the JSON-RPC API documentation on xrpl.org.
- **Test coverage.** Test file documentation (Phase 4) is brief and
optional. Test coverage measurement is handled by existing Codecov
integration.
- **Architectural decision records.** Module-level READMEs already exist
for key subsystems. This project adds function/class-level docs, not
system-level design documents.
## 11. Timeline Summary
```
Week 1 Phase 0: Infrastructure PR (tooling, workflows, standards)
Weeks 2-4 Phase 1: Foundation modules (basics, crypto, json, beast)
Weeks 4-8 Phase 2: Protocol & TX engine (protocol, ledger, tx, paths)
Weeks 8-13 Phase 3: Server & application (overlay, consensus, rpc, app)
Weeks 13-15 Phase 4: Tests & polish, promote to required check
```
**Total duration:** 15 weeks
**Total effort:** ~1 FTE + 80-120 hours reviewer time
**Ongoing cost:** ~$50-150/month API + negligible CI compute

View File

@@ -89,3 +89,30 @@ add_custom_target(
DEPENDS "${doxygen_index_file}"
SOURCES "${dependencies}"
)
# Documentation coverage target using coverxygen.
# Generates LCOV-format coverage report from Doxygen XML output.
# Requires: pip install coverxygen
set(doxygen_xml_dir "${doxygen_output_directory}/xml")
set(doc_coverage_file "${CMAKE_BINARY_DIR}/doc-coverage.info")
add_custom_command(
OUTPUT "${doc_coverage_file}"
COMMAND
coverxygen
--xml-dir "${doxygen_xml_dir}"
--src-dir "${CMAKE_CURRENT_SOURCE_DIR}"
--output "${doc_coverage_file}"
--kind class,struct,function,enum,typedef,variable
--scope public
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS docs
COMMENT "Generating documentation coverage report"
)
add_custom_target(
docs-coverage
DEPENDS "${doc_coverage_file}"
COMMAND
"${CMAKE_COMMAND}" -E echo
"Documentation coverage report: ${doc_coverage_file}"
)

View File

@@ -0,0 +1,192 @@
# XRPLD Documentation Standards
This document defines the canonical format for inline code documentation in the
xrpld codebase. All new and updated code must follow these standards.
## Comment Style
Use Javadoc-style Doxygen comments (`/** ... */`). This matches the dominant
convention in the codebase: ~5,200 existing instances across 569 files.
```cpp
/** Brief description of the entity. */
```
For multi-line documentation, each line is prefixed with ` * ` (space, asterisk,
space):
```cpp
/** Brief description of the entity.
*
* Extended description with behavioral details, invariants,
* and constraints that are not obvious from the signature.
*/
```
`JAVADOC_AUTOBRIEF = YES` is enabled in the Doxyfile, so the first sentence
of any `/** */` block is automatically treated as the brief. An explicit
`@brief` tag is accepted but not required.
The `///` triple-slash style appears in ~37 files (340 instances). It is
valid Doxygen and will not be removed where it exists, but new code should
use `/** */` for consistency with the majority style.
## What to Document
### File-Level (Optional)
The `@file` tag is not currently used anywhere in the codebase. Adding
file-level documentation is encouraged for complex modules where a
high-level overview helps, but it is not required:
```cpp
/** @file
* Defines the Payment transactor for the XRP Ledger.
*
* The Payment transactor handles direct XRP transfers, cross-currency
* payments via the pathfinding engine, and partial payments.
*/
```
Module-level READMEs (e.g., `src/xrpld/peerfinder/README.md`) remain the
primary place for architectural documentation.
### Class / Struct Level
Every class and struct gets a doc block describing:
- What it does (1-2 sentences)
- Key invariants or constraints, if any
- Thread-safety guarantees, if relevant
- Lifecycle notes, if relevant
```cpp
/** Executes a Payment transaction on the XRP Ledger.
*
* Supports direct XRP payments, cross-currency payments via RippleCalc,
* and partial payments (tfPartialPayment). Path count is limited to 6
* with max path length of 8.
*/
class Payment : public Transactor { ... };
```
Target: 2-5 lines for most classes. Complex classes may need more.
### Public Methods and Free Functions
Every public method and free function in headers gets:
- Brief description of behavior (not a restatement of the signature)
- `@param` for each parameter
- `@return` describing what is returned
- `@throw` if it can throw (either `@throw` or `@throws` is acceptable —
the codebase uses both)
- `@note` for non-obvious constraints or edge cases
When a `@param` description wraps, continuation lines are indented with
4 spaces from the `*`:
```cpp
/** Round a Number value to the precision of a given asset.
*
* For IOUs, rounds to the IOU's scale. For XRP and MPT, no rounding
* is performed.
*
* @param asset The relevant asset
* @param value The value to be rounded
* @param scale An exponent value to establish the precision limit of
* `value`. Should be larger than `value.exponent()`.
* @return The rounded Number.
*/
[[nodiscard]] Number
roundToAsset(Asset const& asset, Number const& value, int scale);
```
Target: 1-3 lines of description plus `@param`/`@return`.
### Private Methods
Document private methods only when the logic is non-obvious. A brief
one-line comment is sufficient.
### Enums and Constants
All enums get a brief class-level description. Individual enum values get
inline documentation when the meaning is not self-evident:
```cpp
/** Result codes for transaction processing. */
enum TERCode
{
tesSUCCESS = 0, /**< Transaction succeeded. */
tecCLAIM = 100, /**< Fee claimed; transaction failed. */
tecPATH_PARTIAL = 101, /**< Path could not deliver full amount. */
};
```
## What NOT to Document
- Do not paraphrase the function signature. `/** Returns the account ID. */`
on `AccountID getAccountID()` adds zero information.
- Do not document what is obvious from well-named identifiers.
- Do not reference specific issues, PRs, or task numbers. These belong in
commit messages and rot as the codebase evolves.
- Do not add multi-paragraph docstrings. If it takes that long to explain,
the code may need restructuring.
- Do not document `.cpp` implementation files exhaustively. Focus docs on
headers where the public interface is defined.
## Quality Over Quantity
Wrong documentation is worse than no documentation. Every doc comment must
accurately describe the current behavior. When in doubt:
- Read the implementation before writing the doc
- Cross-reference against test files for edge cases
- Use `@note` to flag subtle behavior that has caught contributors before
## Doxygen Tags Reference
Tags in regular use across the codebase:
| Tag | Codebase Usage | Purpose |
|-----|----------------|---------|
| `@brief` | ~2,500 instances | Brief description (optional — autobrief is enabled) |
| `@param` | ~2,400 instances | Function parameter description |
| `@return` | ~2,200 instances | Return value description |
| `@note` | ~270 instances | Important behavioral note or caveat |
| `@throw` / `@throws` | ~450 instances combined | Exception specification |
| `@see` | ~64 instances | Cross-reference to related entities |
| `@tparam` | ~43 instances | Template parameter description |
Tags used rarely but accepted:
| Tag | Codebase Usage | Purpose |
|-----|----------------|---------|
| `@invariant` | ~13 instances | Property that must always hold |
| `@pre` | ~3 instances | Precondition |
| `@file` | 0 instances | File-level description (new convention, optional) |
## Enforcement
Documentation coverage is measured by [coverxygen](https://github.com/psycofdj/coverxygen)
and enforced in CI:
- PRs cannot decrease documentation coverage (`no_decrease` ratchet mode)
- New files added in a PR require >= 80% doc coverage
- Module-specific thresholds increase quarterly
- The doc-review GitHub Action checks whether code changes invalidate
existing documentation
Coverage is measured against public API surface: classes, structs,
functions, enums, typedefs, and variables. Private implementation details
are not counted.
## Style Notes
- Doc comments go immediately before the entity they describe (no blank
line between the comment and the declaration)
- Keep `@param` descriptions on a single line when possible
- For wrapped `@param` descriptions, indent continuation lines 4 spaces
from the `*`
- Use `@see` sparingly — only when the relationship is non-obvious
- Code style (braces, line width, formatting) is governed by `.clang-format`
and is independent of these documentation standards

View File

@@ -49,7 +49,7 @@ LOOKUP_CACHE_SIZE = 0
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
EXTRACT_ALL = YES
EXTRACT_ALL = NO
EXTRACT_PRIVATE = YES
EXTRACT_PACKAGE = NO
EXTRACT_STATIC = YES
@@ -257,7 +257,7 @@ MAN_LINKS = NO
#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------
GENERATE_XML = NO
GENERATE_XML = YES
XML_OUTPUT = xml
XML_PROGRAMLISTING = YES