mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 00:36:48 +00:00
github workflows
This commit is contained in:
29
.github/doc-coverage-thresholds.json
vendored
Normal file
29
.github/doc-coverage-thresholds.json
vendored
Normal 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
277
.github/scripts/doc-coverage-check.py
vendored
Normal 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
279
.github/scripts/doc-review.py
vendored
Normal 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
109
.github/workflows/doc-coverage.yml
vendored
Normal 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
98
.github/workflows/doc-review.yml
vendored
Normal 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
345
SCOPE_OF_WORK.md
Normal 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
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
192
docs/DOCUMENTATION_STANDARDS.md
Normal file
192
docs/DOCUMENTATION_STANDARDS.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user