
Diataxis
- 1 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Audits, classifies, validates, and scaffolds documentation using the Diataxis framework of tutorials, how-to, reference, and explanation.
About
Applies the Diataxis framework via scripts to classify docs into quadrants, audit coverage, validate quadrant purity, and scaffold structure. A developer uses it when organizing documentation by type or checking for collapsed mixed-quadrant docs.
- Four scripts: classify, audit, validate, scaffold
- Multi-signal weighted classification with DX001-DX010 validation rules
Diataxis by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,361 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joaquimscosta/arkhe-claude-plugins --skill diataxisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 5, 2026 |
| Repository | joaquimscosta/arkhe-claude-plugins ↗ |
What it does
Audits, classifies, validates, and scaffolds documentation using the Diataxis framework of tutorials, how-to, reference, and explanation.
Files
Diataxis Documentation Framework
Audit, classify, validate, and scaffold documentation using the Diataxis framework.
Quick Start
# Classify individual files
uv run scripts/diataxis_classify.py docs/*.md
# Audit a docs directory for coverage
uv run scripts/diataxis_audit.py --dir docs
# Validate quadrant purity
uv run scripts/diataxis_validate.py --dir docs
# Scaffold a new Diataxis structure
uv run scripts/diataxis_scaffold.py --dry-run
uv run scripts/diataxis_scaffold.pyCapabilities
| Script | Purpose | Key Flags |
|---|---|---|
diataxis_classify.py | Classify files into quadrants | --json, --verbose, --no-content |
diataxis_audit.py | Coverage report with quality score | --dir, --json, --min-coverage |
diataxis_validate.py | Lint for quadrant purity (DX001-DX010) | --dir, --file, --strict, --json |
diataxis_scaffold.py | Generate folder structure | `--layout folders\ |
The Four Quadrants
| Quadrant | Orientation | User State | Folder |
|---|---|---|---|
| Tutorial | Learning | Study + Action | tutorials/ |
| How-to | Task | Work + Action | how-to/ |
| Reference | Information | Work + Cognition | reference/ |
| Explanation | Understanding | Study + Cognition | explanation/ |
Classification Algorithm
Multi-signal weighted scoring (title 30%, headings 25%, content 25%, structure 20%). Documents scoring highly for 2+ quadrants are flagged as "collapsed" with split suggestions.
Validation Rules
| ID | Rule | Severity |
|---|---|---|
| DX001 | Tutorial contains reference tables | warning |
| DX002 | How-to has long conceptual preamble | warning |
| DX003 | Reference contains step-by-step instructions | warning |
| DX004 | Explanation contains execution commands | warning |
| DX005 | No clear quadrant signal | info |
| DX006 | Collapsed document (mixed quadrants) | warning |
| DX007 | Tutorial missing prerequisites | info |
| DX008 | Tutorial missing learning objectives | info |
| DX009 | How-to missing problem statement | info |
| DX010 | Reference missing tables | info |
Config File (.diataxis-config.json)
Optional per-project override:
{
"version": 1,
"root": "docs",
"layout": "folders",
"ignore": ["node_modules", ".git", "adr", "rfcs", "*.pdf"],
"custom_signals": {}
}Create with uv run scripts/diataxis_scaffold.py --init-config.
Common Issues
| Issue | Fix |
|---|---|
uv not found | `curl -LsSf https://astral.sh/uv/install.sh \ |
| Low confidence on all files | Files may lack quadrant-specific keywords; use --verbose to inspect scores |
| Too many collapsed warnings | Some docs legitimately mix quadrants; consider splitting or accepting |
See TROUBLESHOOTING.md for all error scenarios.
References
- WORKFLOW.md — Full methodology (discover, classify, audit, validate, scaffold)
- EXAMPLES.md — Real-world examples for all operations
- TROUBLESHOOTING.md — Error handling and debugging tips
- Diataxis framework — Official documentation
Diataxis Examples
Real-world usage examples for all Diataxis skill operations.
---
Example 1: Classify a Project's Documentation
Scenario: You have a project with 8 markdown files in docs/ and want to understand their Diataxis breakdown.
$ uv run scripts/diataxis_classify.py docs/*.md --verbose
File | Quadrant | Confidence | Collapsed | Reason
----------------------+-------------+------------+-----------+----------------------------
getting-started.md | tutorial | high | No | Title: 'getting-started'; Structure: numbered_steps(12)
deploy-guide.md | how-to | high | No | Title: 'deploy'; Content: 'run the following'
api-reference.md | reference | high | No | Title: 'reference'; Structure: tables(15), parameter_tables(3)
architecture.md | explanation | medium | Yes (E+R) | Title: 'architecture'; Structure: long_paragraphs(4)
config-options.md | reference | high | No | Title: 'config'; Structure: parameter_tables(2)
setup-guide.md | how-to | medium | No | Title: 'setup', 'guide'; Content: 'configure the'
design-decisions.md | explanation | high | No | Title: 'design'; Content: 'the reason', 'the tradeoff'
README.md | tutorial | low | No | Structure: numbered_steps(3)
Scores: explanation: 0.00, how-to: 0.25, reference: 0.20, tutorial: 0.30
Summary: 8 files classified
tutorial: 2
how-to: 2
reference: 2
explanation: 2
collapsed: 1 (mixed quadrants)Key observations:
architecture.mdis collapsed (mixes Explanation + Reference) — consider splittingREADME.mdhas low confidence — it's a mixed-purpose file, which is normal for READMEs
---
Example 2: Audit Documentation Coverage
Scenario: Run a coverage audit to find gaps.
$ uv run scripts/diataxis_audit.py --dir docs
Diataxis Documentation Audit
========================================
Directory: /projects/my-app/docs
Total documents: 8
Quadrant Coverage
--------------------
Tutorial : ██░░░░░░░░ 2 docs (25%) [getting-started.md, README.md]
How-to : ██░░░░░░░░ 2 docs (25%) [deploy-guide.md, setup-guide.md]
Reference : ██░░░░░░░░ 2 docs (25%) [api-reference.md, config-options.md]
Explanation : ██░░░░░░░░ 2 docs (25%) [architecture.md, design-decisions.md]
Collapsed Documents (mixed quadrants)
--------------------
! architecture.md — mixes Explanation + Reference
Suggestion: Split into architecture-explanation.md and architecture-reference.md
Quality Score: 73/100
Coverage balance: 25/25
Quadrant purity: 22/25
Classification confidence: 18/25
Documentation volume: 8/25
Result: PASS (8 docs, 1 collapsed, 0 unclassified)---
Example 3: Validate Quadrant Purity
Scenario: Check that your tutorial doesn't accidentally include reference content.
$ uv run scripts/diataxis_validate.py --file docs/getting-started.md
Diataxis Purity Validation Report
========================================
Directory: /projects/my-app/docs
Files validated: 1
Info: 1
- DX008 getting-started.md — Tutorial missing 'What You'll Learn' section
Result: PASS (1 files, 0 errors, 0 warnings, 1 info)Scenario: Validate all docs for CI.
$ uv run scripts/diataxis_validate.py --dir docs --strict
Diataxis Purity Validation Report
========================================
Directory: /projects/my-app/docs
Files validated: 8
Warnings: 2
! DX001 getting-started.md (line 85) — Tutorial contains 8 table rows (reference-style content)
Suggestion: Move parameter/option tables to a separate Reference document
! DX006 architecture.md — Collapsed document: mixes Explanation + Reference
Suggestion: Consider splitting into separate explanation and reference documents
Info: 1
- DX008 getting-started.md — Tutorial missing 'What You'll Learn' section
Result: WARN (8 files, 0 errors, 2 warnings, 1 info)With --strict, exit code is 1 (CI fails on warnings).
---
Example 4: Scaffold a New Diataxis Structure
Scenario: Set up Diataxis folders for a new project.
$ uv run scripts/diataxis_scaffold.py --dry-run
Scaffolding Diataxis structure at: docs/ [folders] (dry-run)
Would create: docs/
Would create: docs/README.md
Would create: docs/tutorials/
Would create: docs/tutorials/README.md
Would create: docs/how-to/
Would create: docs/how-to/README.md
Would create: docs/reference/
Would create: docs/reference/README.md
Would create: docs/explanation/
Would create: docs/explanation/README.md
Would create: 10 items$ uv run scripts/diataxis_scaffold.py --init-config
Scaffolding Diataxis structure at: docs/ [folders]
...
Created: 10 items
Next steps:
1. Add documentation to each quadrant directory
2. Run: uv run diataxis_audit.py --dir docs to check coverage
3. Run: uv run diataxis_validate.py --dir docs to check purityFlat layout for smaller projects:
$ uv run scripts/diataxis_scaffold.py --layout flat --dry-run
Scaffolding Diataxis structure at: docs/ [flat] (dry-run)
Would create: docs/
Would create: docs/README.md
Would create: 2 items---
Example 5: JSON Output for Scripting
Scenario: Pipe classification results to another tool.
$ uv run scripts/diataxis_classify.py docs/api-reference.md --json
[
{
"file": "/projects/my-app/docs/api-reference.md",
"primary_quadrant": "reference",
"confidence": "high",
"score": 0.85,
"scores": {
"tutorial": 0.0,
"how-to": 0.1,
"reference": 0.85,
"explanation": 0.05
},
"is_collapsed": false,
"collapsed_quadrants": [],
"signals": {
"reference": [
"title:reference",
"title:api",
"heading:parameters",
"heading:endpoints",
"structural:tables(15)",
"structural:parameter_tables(3)"
]
},
"reason": "Title: 'reference', 'api'; Structure: tables(15), parameter_tables(3)"
}
]---
Example 6: Handling Collapsed Documents
Scenario: architecture.md is flagged as collapsed (mixes Explanation + Reference).
Step 1: Understand why:
$ uv run scripts/diataxis_classify.py docs/architecture.md --verbose
File | Quadrant | Confidence | Collapsed | Reason
------------------+-------------+------------+-----------+---------
architecture.md | explanation | medium | Yes (E+R) | Title: 'architecture'; Structure: long_paragraphs(4)
Scores: explanation: 0.45, how-to: 0.10, reference: 0.35, tutorial: 0.00Both explanation (0.45) and reference (0.35) score above 0.3, and the ratio is < 2:1.
Step 2: Read the doc and identify sections to split:
- Conceptual sections ("Why we chose microservices", "Design principles") →
explanation/architecture-overview.md - Factual sections ("Service endpoints", "Configuration matrix") →
reference/architecture-reference.md
Step 3: Re-validate after splitting:
$ uv run scripts/diataxis_validate.py --file docs/explanation/architecture-overview.md
# Should pass with no DX006 warning#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Audit documentation coverage across Diataxis quadrants.
Scans a docs directory, classifies every document, and produces a
coverage report with quadrant distribution, gaps, and quality score.
Usage:
uv run diataxis_audit.py --dir docs
uv run diataxis_audit.py --dir docs --json
uv run diataxis_audit.py --dir docs --min-coverage 2
"""
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from shared import (
QUADRANTS,
ClassificationResult,
format_bar,
resolve_config,
scan_markdown_files,
)
from diataxis_classify import classify_file
# ---------------------------------------------------------------------------
# Coverage analysis
# ---------------------------------------------------------------------------
def compute_coverage(
results: list[ClassificationResult],
) -> dict[str, list[ClassificationResult]]:
"""Group classification results by quadrant."""
coverage: dict[str, list[ClassificationResult]] = {
q: [] for q in QUADRANTS
}
coverage["unclassified"] = []
for r in results:
bucket = r.primary_quadrant if r.primary_quadrant in QUADRANTS else "unclassified"
coverage[bucket].append(r)
return coverage
def compute_quality_score(
results: list[ClassificationResult],
coverage: dict[str, list[ClassificationResult]],
) -> dict[str, int]:
"""Compute a 0-100 quality score with four components (25 pts each)."""
total = len(results)
if total == 0:
return {"coverage_balance": 0, "quadrant_purity": 0,
"classification_confidence": 0, "documentation_volume": 0, "total": 0}
# 1. Coverage balance (25 pts) — how evenly distributed
quadrant_counts = [len(coverage[q]) for q in QUADRANTS]
non_zero = sum(1 for c in quadrant_counts if c > 0)
if non_zero == 0:
balance_score = 0
elif non_zero == 4:
# All quadrants present — score by evenness
ideal = total / 4
deviation = sum(abs(c - ideal) for c in quadrant_counts) / total
balance_score = max(0, round(25 * (1 - deviation)))
else:
# Partial coverage — proportional
balance_score = round(25 * non_zero / 4)
# 2. Quadrant purity (25 pts) — penalize collapsed docs
collapsed = sum(1 for r in results if r.is_collapsed)
if total > 0:
purity_ratio = 1 - (collapsed / total)
purity_score = round(25 * purity_ratio)
else:
purity_score = 25
# 3. Classification confidence (25 pts) — average confidence
confidence_values = {"high": 1.0, "medium": 0.6, "low": 0.2}
if total > 0:
avg_conf = sum(confidence_values.get(r.confidence, 0) for r in results) / total
confidence_score = round(25 * avg_conf)
else:
confidence_score = 0
# 4. Documentation volume (25 pts) — scale by total count
# 1 doc = 5 pts, up to 5+ per quadrant = 25 pts
volume_score = min(25, total * 5 // max(non_zero, 1))
total_score = balance_score + purity_score + confidence_score + volume_score
return {
"coverage_balance": balance_score,
"quadrant_purity": purity_score,
"classification_confidence": confidence_score,
"documentation_volume": volume_score,
"total": total_score,
}
def find_gaps(
coverage: dict[str, list[ClassificationResult]],
total: int,
) -> list[dict[str, str]]:
"""Identify underrepresented quadrants."""
gaps: list[dict[str, str]] = []
suggestions: dict[str, str] = {
"tutorial": "getting started guides, step-by-step lessons, or walkthrough documents",
"how-to": "task-oriented guides like 'How to deploy' or 'How to configure X'",
"reference": "API reference, configuration options, CLI commands, or parameter tables",
"explanation": "architecture overviews, design rationale, or 'Why we chose X' docs",
}
for quadrant in QUADRANTS:
count = len(coverage[quadrant])
if count == 0:
gaps.append({
"quadrant": quadrant,
"severity": "missing",
"message": f"No {quadrant} documents found",
"suggestion": f"Consider writing: {suggestions[quadrant]}",
})
elif total > 0 and count / total < 0.10:
pct = round(count / total * 100)
gaps.append({
"quadrant": quadrant,
"severity": "underrepresented",
"message": f"{quadrant.capitalize()} is underrepresented ({count} docs, {pct}%)",
"suggestion": f"Consider adding: {suggestions[quadrant]}",
})
return gaps
# ---------------------------------------------------------------------------
# Output formatters
# ---------------------------------------------------------------------------
def format_report(
results: list[ClassificationResult],
coverage: dict[str, list[ClassificationResult]],
quality: dict[str, int],
gaps: list[dict[str, str]],
collapsed_results: list[ClassificationResult],
docs_dir: Path,
) -> str:
"""Format a human-readable audit report."""
total = len(results)
lines = [
"Diataxis Documentation Audit",
"=" * 40,
f"Directory: {docs_dir}",
f"Total documents: {total}",
"",
"Quadrant Coverage",
"-" * 20,
]
for quadrant, meta in QUADRANTS.items():
docs = coverage[quadrant]
count = len(docs)
pct = round(count / total * 100) if total > 0 else 0
bar = format_bar(count, total)
file_names = ", ".join(r.file_path.name for r in docs[:3])
if len(docs) > 3:
file_names += f", ... (+{len(docs) - 3} more)"
label = f"{quadrant.capitalize():<12}"
lines.append(f" {label}: {bar} {count} docs ({pct}%) [{file_names}]")
# Unclassified
unclassified = coverage.get("unclassified", [])
if unclassified:
count = len(unclassified)
pct = round(count / total * 100) if total > 0 else 0
bar = format_bar(count, total)
file_names = ", ".join(r.file_path.name for r in unclassified[:3])
if len(unclassified) > 3:
file_names += f", ... (+{len(unclassified) - 3} more)"
lines.append(f" {'Unclassified':<12}: {bar} {count} docs ({pct}%) [{file_names}]")
lines.append("")
# Gaps
if gaps:
lines.append("Coverage Gaps")
lines.append("-" * 20)
for gap in gaps:
marker = "!" if gap["severity"] == "missing" else "-"
lines.append(f" {marker} {gap['message']}")
lines.append(f" {gap['suggestion']}")
lines.append("")
# Collapsed documents
if collapsed_results:
lines.append("Collapsed Documents (mixed quadrants)")
lines.append("-" * 20)
for r in collapsed_results:
mixed = " + ".join(q.capitalize() for q in r.collapsed_quadrants)
lines.append(f" ! {r.file_path.name} — mixes {mixed}")
# Generate split suggestion
quads = r.collapsed_quadrants
if len(quads) == 2:
stem = r.file_path.stem
lines.append(
f" Suggestion: Split into {stem}-{quads[0]}.md "
f"({quads[0]}) and {stem}-{quads[1]}.md ({quads[1]})"
)
lines.append("")
# Quality score
lines.append("Quality Score: {}/100".format(quality["total"]))
lines.append(f" Coverage balance: {quality['coverage_balance']}/25")
lines.append(f" Quadrant purity: {quality['quadrant_purity']}/25")
lines.append(f" Classification confidence: {quality['classification_confidence']}/25")
lines.append(f" Documentation volume: {quality['documentation_volume']}/25")
lines.append("")
# Result
result = "PASS" if quality["total"] >= 50 else "NEEDS IMPROVEMENT"
collapsed_count = len(collapsed_results)
unclassified_count = len(unclassified)
lines.append(
f"Result: {result} ({total} docs, {collapsed_count} collapsed, "
f"{unclassified_count} unclassified)"
)
return "\n".join(lines)
def format_audit_json(
results: list[ClassificationResult],
coverage: dict[str, list[ClassificationResult]],
quality: dict[str, int],
gaps: list[dict[str, str]],
docs_dir: Path,
) -> str:
"""Format audit results as JSON."""
data = {
"directory": str(docs_dir),
"total_documents": len(results),
"coverage": {
quadrant: {
"count": len(docs),
"percentage": round(len(docs) / len(results) * 100) if results else 0,
"files": [str(r.file_path) for r in docs],
}
for quadrant, docs in coverage.items()
},
"gaps": gaps,
"collapsed": [
{
"file": str(r.file_path),
"quadrants": r.collapsed_quadrants,
}
for r in results if r.is_collapsed
],
"quality_score": quality,
"classifications": [
{
"file": str(r.file_path),
"quadrant": r.primary_quadrant,
"confidence": r.confidence,
"score": round(r.score, 2),
"is_collapsed": r.is_collapsed,
}
for r in results
],
}
return json.dumps(data, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description="Audit documentation coverage across Diataxis quadrants"
)
parser.add_argument(
"--dir",
"-d",
required=True,
help="Docs directory to audit",
)
parser.add_argument(
"--min-coverage",
type=int,
default=1,
help="Minimum docs per quadrant to pass (default: 1)",
)
parser.add_argument(
"--config",
"-c",
default=None,
help="Path to .diataxis-config.json",
)
parser.add_argument(
"--json",
action="store_true",
dest="json_output",
help="Output results as JSON",
)
args = parser.parse_args()
base_path = Path.cwd()
config = resolve_config(args.config, base_path)
# Resolve directory
docs_dir = Path(args.dir)
if not docs_dir.is_absolute():
docs_dir = base_path / docs_dir
if not docs_dir.exists():
print(f"Error: Directory does not exist: {docs_dir}")
return 1
# Scan and classify
ignore = config.get("ignore", [])
files = scan_markdown_files(docs_dir, ignore)
if not files:
print(f"No markdown files found in {docs_dir}")
return 0
results = [classify_file(f, config) for f in files]
# Analyze
coverage = compute_coverage(results)
quality = compute_quality_score(results, coverage)
gaps = find_gaps(coverage, len(results))
collapsed_results = [r for r in results if r.is_collapsed]
# Output
if args.json_output:
print(format_audit_json(results, coverage, quality, gaps, docs_dir))
else:
print(format_report(results, coverage, quality, gaps, collapsed_results, docs_dir))
# Check minimum coverage
failed = False
for quadrant in QUADRANTS:
if len(coverage[quadrant]) < args.min_coverage:
failed = True
if not args.json_output:
print(f"\nNote: {quadrant} has fewer than {args.min_coverage} doc(s)")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Classify markdown files into Diataxis quadrants using multi-signal heuristics.
Usage:
uv run diataxis_classify.py docs/getting-started.md
uv run diataxis_classify.py docs/*.md
uv run diataxis_classify.py docs/*.md --verbose
uv run diataxis_classify.py docs/*.md --json
uv run diataxis_classify.py docs/*.md --no-content
"""
import argparse
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from shared import (
QUADRANT_SIGNALS,
QUADRANTS,
ClassificationResult,
format_confidence,
read_document,
resolve_config,
)
# ---------------------------------------------------------------------------
# Classification engine
# ---------------------------------------------------------------------------
def _match_title_keywords(
stem: str,
segments: set[str],
keywords: list[str],
) -> tuple[float, list[str]]:
"""Match filename/title against keyword list. Max score 0.30."""
matched: list[str] = []
for kw in keywords:
if kw in stem or kw in segments:
matched.append(kw)
if not matched:
return 0.0, []
score = min(0.15 + 0.05 * (len(matched) - 1), 0.30)
return score, matched
def _match_heading_patterns(
headings: list[str],
patterns: list[str],
) -> tuple[float, list[str]]:
"""Match document headings against regex patterns. Max score 0.25."""
matched: list[str] = []
for heading in headings:
for pattern in patterns:
if re.search(pattern, heading, re.IGNORECASE):
tag = f"heading:{heading[:40]}"
if tag not in matched:
matched.append(tag)
if not matched:
return 0.0, []
score = min(0.10 + 0.05 * (len(matched) - 1), 0.25)
return score, matched
def _match_content_keywords(
body: str,
keywords: list[str],
) -> tuple[float, list[str]]:
"""Match body text against content keyword phrases. Max score 0.25."""
matched: list[str] = []
for kw in keywords:
if kw in body:
matched.append(kw)
if not matched:
return 0.0, []
score = min(0.05 * len(matched), 0.25)
return score, matched
def _analyze_structure(filepath: Path, max_lines: int = 200) -> dict[str, int]:
"""Analyze structural features of a document."""
features: dict[str, int] = {
"numbered_steps": 0,
"tables": 0,
"code_blocks": 0,
"long_paragraphs": 0,
"parameter_tables": 0,
"command_blocks": 0,
}
try:
with open(filepath) as f:
in_code_block = False
paragraph_words = 0
code_block_content: list[str] = []
table_header_seen = False
for i, line in enumerate(f):
if i >= max_lines:
break
stripped = line.strip()
# Track code blocks
if stripped.startswith("```"):
if in_code_block:
# Closing code block — check if it contains commands
block_text = " ".join(code_block_content).lower()
if any(cmd in block_text for cmd in [
"run ", "install ", "npm ", "pip ", "docker ",
"curl ", "wget ", "git ", "cd ", "mkdir ",
"uv run", "python ", "node ",
]):
features["command_blocks"] += 1
code_block_content = []
in_code_block = not in_code_block
features["code_blocks"] += 1
continue
if in_code_block:
code_block_content.append(stripped)
continue
# Numbered steps
if re.match(r"^\d+\.\s+", stripped) or re.match(r"^step\s+\d+", stripped, re.IGNORECASE):
features["numbered_steps"] += 1
# Tables
if "|" in stripped and stripped.startswith("|"):
if not table_header_seen:
table_header_seen = True
# Check if it looks like a parameter table
lower_line = stripped.lower()
if any(col in lower_line for col in [
"type", "default", "required", "description",
"parameter", "option", "flag",
]):
features["parameter_tables"] += 1
features["tables"] += 1
else:
table_header_seen = False
# Long paragraphs (non-heading, non-list, non-empty text)
if stripped and not stripped.startswith(("#", "-", "*", ">", "|", "!")):
paragraph_words += len(stripped.split())
else:
if paragraph_words > 80:
features["long_paragraphs"] += 1
paragraph_words = 0
# Final paragraph check
if paragraph_words > 80:
features["long_paragraphs"] += 1
except (OSError, UnicodeDecodeError):
pass
return features
def _score_structural_signals(
features: dict[str, int],
quadrant: str,
) -> tuple[float, list[str]]:
"""Score structural features for a specific quadrant. Max score 0.20."""
matched: list[str] = []
if quadrant == "tutorial":
if features["numbered_steps"] >= 3:
matched.append(f"numbered_steps({features['numbered_steps']})")
if features["code_blocks"] >= 2 and features["long_paragraphs"] <= 2:
matched.append("incremental_code")
elif quadrant == "how-to":
if features["command_blocks"] >= 1:
matched.append(f"command_blocks({features['command_blocks']})")
if features["numbered_steps"] >= 1 and features["long_paragraphs"] == 0:
matched.append("short_steps")
elif quadrant == "reference":
if features["tables"] >= 3:
matched.append(f"tables({features['tables']})")
if features["parameter_tables"] >= 1:
matched.append(f"parameter_tables({features['parameter_tables']})")
if features["code_blocks"] >= 1 and features["long_paragraphs"] == 0:
matched.append("code_signatures")
elif quadrant == "explanation":
if features["long_paragraphs"] >= 2:
matched.append(f"long_paragraphs({features['long_paragraphs']})")
if features["code_blocks"] <= 1 and features["long_paragraphs"] >= 1:
matched.append("narrative_flow")
if not matched:
return 0.0, []
score = min(0.10 + 0.05 * (len(matched) - 1), 0.20)
return score, matched
def classify_file(
filepath: Path,
config: dict,
scan_content: bool = True,
) -> ClassificationResult:
"""Classify a file by combining four signal sources."""
result = ClassificationResult(file_path=filepath)
# Prepare filename for matching
stem = filepath.stem.lower()
normalized_stem = re.sub(r"[_ ]+", "-", stem)
segments = set(normalized_stem.split("-"))
# Read document content
if scan_content and filepath.suffix.lower() == ".md" and filepath.is_file():
title, body, headings = read_document(filepath)
else:
title, body, headings = "", "", []
# Include title in matching
if title:
title_stem = re.sub(r"[_ ]+", "-", title)
title_segments = set(title_stem.split("-"))
combined_stem = f"{normalized_stem} {title_stem}"
combined_segments = segments | title_segments
else:
combined_stem = normalized_stem
combined_segments = segments
# Analyze structure once (reused for all quadrants)
features = _analyze_structure(filepath) if scan_content else {}
# Score each quadrant
quadrant_scores: dict[str, float] = {}
quadrant_signals: dict[str, list[str]] = {}
for quadrant, signals in QUADRANT_SIGNALS.items():
all_signals: list[str] = []
total_score = 0.0
signal_types_matched = 0
# Signal 1: Title/filename keywords (max 0.30)
s1_score, s1_matches = _match_title_keywords(
combined_stem, combined_segments, signals["title_keywords"],
)
if s1_matches:
all_signals.extend(f"title:{m}" for m in s1_matches)
signal_types_matched += 1
total_score += s1_score
# Signal 2: Heading patterns (max 0.25)
if headings:
s2_score, s2_matches = _match_heading_patterns(
headings, signals["heading_patterns"],
)
if s2_matches:
all_signals.extend(s2_matches)
signal_types_matched += 1
total_score += s2_score
# Signal 3: Content keywords (max 0.25)
if body:
s3_score, s3_matches = _match_content_keywords(
body, signals["content_keywords"],
)
if s3_matches:
all_signals.extend(f"content:{m}" for m in s3_matches)
signal_types_matched += 1
total_score += s3_score
# Signal 4: Structural analysis (max 0.20)
if features:
s4_score, s4_matches = _score_structural_signals(features, quadrant)
if s4_matches:
all_signals.extend(f"structural:{m}" for m in s4_matches)
signal_types_matched += 1
total_score += s4_score
# Agreement bonus when 3+ signal types converge
if signal_types_matched >= 3:
total_score = min(total_score + 0.10, 1.0)
quadrant_scores[quadrant] = round(total_score, 3)
quadrant_signals[quadrant] = all_signals
result.scores = quadrant_scores
# Pick the best quadrant
if not any(s > 0 for s in quadrant_scores.values()):
result.reason = "No keyword matches"
return result
best = max(quadrant_scores, key=lambda k: quadrant_scores[k])
result.primary_quadrant = best
result.score = quadrant_scores[best]
result.confidence = format_confidence(result.score)
result.signals = {q: sigs for q, sigs in quadrant_signals.items() if sigs}
# Collapsed document detection
above_threshold = [q for q, s in quadrant_scores.items() if s >= 0.3]
if len(above_threshold) >= 2:
sorted_scores = sorted(quadrant_scores.values(), reverse=True)
ratio = sorted_scores[0] / sorted_scores[1] if sorted_scores[1] > 0 else float("inf")
if ratio < 2.0:
result.is_collapsed = True
result.collapsed_quadrants = sorted(above_threshold)
# Build reason string
reasons: list[str] = []
best_signals = quadrant_signals.get(best, [])
title_sigs = [s.split(":", 1)[1] for s in best_signals if s.startswith("title:")]
structural_sigs = [s.split(":", 1)[1] for s in best_signals if s.startswith("structural:")]
content_sigs = [s.split(":", 1)[1] for s in best_signals if s.startswith("content:")]
heading_sigs = [s.split(":", 1)[1] for s in best_signals if s.startswith("heading:")]
if title_sigs:
reasons.append(f"Title: {', '.join(repr(k) for k in title_sigs[:3])}")
if heading_sigs:
reasons.append(f"Headings: {', '.join(h[:25] for h in heading_sigs[:2])}")
if structural_sigs:
reasons.append(f"Structure: {', '.join(structural_sigs[:2])}")
if content_sigs:
reasons.append(f"Content: {', '.join(repr(k) for k in content_sigs[:2])}")
result.reason = "; ".join(reasons) if reasons else "Weak keyword match"
return result
# ---------------------------------------------------------------------------
# Output formatters
# ---------------------------------------------------------------------------
def format_table(results: list[ClassificationResult], verbose: bool = False) -> str:
"""Format classification results as a human-readable table."""
if not results:
return "No files to classify."
file_w = max(len("File"), max(len(r.file_path.name) for r in results))
quad_w = max(len("Quadrant"), max(len(r.primary_quadrant or "(unknown)") for r in results))
conf_w = len("Confidence")
# Compute collapsed column width accounting for actual content
def _collapsed_str(r: ClassificationResult) -> str:
if r.is_collapsed:
return f"Yes ({'+'.join(q[0].upper() for q in r.collapsed_quadrants)})"
return "No"
coll_w = max(len("Collapsed"), max(len(_collapsed_str(r)) for r in results))
header = (
f"{'File':<{file_w}} | {'Quadrant':<{quad_w}} | "
f"{'Confidence':<{conf_w}} | {'Collapsed':<{coll_w}} | Reason"
)
sep = (
f"{'-' * file_w}-+-{'-' * quad_w}-+-"
f"{'-' * conf_w}-+-{'-' * coll_w}-+{'-' * 30}"
)
lines = [header, sep]
for r in results:
quadrant = r.primary_quadrant or "(unknown)"
collapsed = "Yes" if r.is_collapsed else "No"
if r.is_collapsed:
collapsed = f"Yes ({'+'.join(q[0].upper() for q in r.collapsed_quadrants)})"
lines.append(
f"{r.file_path.name:<{file_w}} | {quadrant:<{quad_w}} | "
f"{r.confidence:<{conf_w}} | {collapsed:<{coll_w}} | {r.reason}"
)
if verbose and r.scores:
scores_str = ", ".join(f"{q}: {s:.2f}" for q, s in sorted(r.scores.items()))
lines.append(f"{'':>{file_w}} Scores: {scores_str}")
return "\n".join(lines)
def format_json(results: list[ClassificationResult]) -> str:
"""Format classification results as JSON."""
data = []
for r in results:
data.append({
"file": str(r.file_path),
"primary_quadrant": r.primary_quadrant,
"confidence": r.confidence,
"score": round(r.score, 2),
"scores": {q: round(s, 2) for q, s in r.scores.items()},
"is_collapsed": r.is_collapsed,
"collapsed_quadrants": r.collapsed_quadrants,
"signals": r.signals,
"reason": r.reason,
})
return json.dumps(data, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description="Classify markdown files into Diataxis quadrants"
)
parser.add_argument(
"files",
nargs="+",
help="File paths to classify",
)
parser.add_argument(
"--no-content",
action="store_true",
help="Filename-only classification (skip content scanning)",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Show all quadrant scores per file",
)
parser.add_argument(
"--config",
"-c",
default=None,
help="Path to .diataxis-config.json",
)
parser.add_argument(
"--json",
action="store_true",
dest="json_output",
help="Output results as JSON",
)
args = parser.parse_args()
base_path = Path.cwd()
config = resolve_config(args.config, base_path)
# Resolve and validate file paths
files: list[Path] = []
for f in args.files:
p = Path(f)
if not p.is_absolute():
p = base_path / p
if not p.exists():
print(f"Warning: File not found: {f}", file=sys.stderr)
continue
if not p.is_file():
continue
files.append(p)
if not files:
print("Error: No valid files to classify")
return 1
# Classify
scan_content = not args.no_content
results = [classify_file(f, config, scan_content) for f in files]
# Output
if args.json_output:
print(format_json(results))
else:
print(format_table(results, verbose=args.verbose))
# Summary (human-readable only)
by_quadrant: dict[str, int] = {}
collapsed = 0
for r in results:
q = r.primary_quadrant or "(unknown)"
by_quadrant[q] = by_quadrant.get(q, 0) + 1
if r.is_collapsed:
collapsed += 1
print(f"\nSummary: {len(results)} files classified")
for q in ["tutorial", "how-to", "reference", "explanation"]:
count = by_quadrant.get(q, 0)
if count:
print(f" {q}: {count}")
unknown = by_quadrant.get("(unknown)", 0)
if unknown:
print(f" unclassified: {unknown}")
if collapsed:
print(f" collapsed: {collapsed} (mixed quadrants)")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Scaffold a Diataxis documentation structure.
Creates a folder layout with README templates for each Diataxis quadrant,
or a flat layout with a hub README and config file.
Usage:
uv run diataxis_scaffold.py --dry-run # Preview
uv run diataxis_scaffold.py # Folders layout
uv run diataxis_scaffold.py --layout flat # Flat layout
uv run diataxis_scaffold.py --root docs --init-config
"""
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from shared import (
DEFAULT_CONFIG,
QUADRANTS,
find_git_root,
resolve_config,
)
# ---------------------------------------------------------------------------
# README templates
# ---------------------------------------------------------------------------
def generate_hub_readme(project_name: str, layout: str) -> str:
"""Generate the docs root README.md content."""
lines = [
f"# {project_name} Documentation",
"",
"Documentation organized using the [Diataxis](https://diataxis.fr/) framework.",
"",
"## Documentation Map",
"",
"| Quadrant | Purpose | Directory |",
"|----------|---------|-----------|",
]
for quadrant, meta in QUADRANTS.items():
folder = meta["folder"]
desc = meta["description"]
if layout == "folders":
lines.append(f"| {quadrant.capitalize()} | {desc} | [`{folder}/`](./{folder}/) |")
else:
lines.append(f"| {quadrant.capitalize()} | {desc} | _(flat layout)_ |")
lines.extend([
"",
"## Quick Start",
"",
"- **New user?** Start with the [tutorials](./tutorials/)",
"- **Need to do something?** Check the [how-to guides](./how-to/)",
"- **Looking up details?** See the [reference](./reference/)",
"- **Want to understand why?** Read the [explanations](./explanation/)",
"",
])
return "\n".join(lines)
def generate_quadrant_readme(quadrant: str) -> str:
"""Generate a per-quadrant README.md content."""
meta = QUADRANTS[quadrant]
title = quadrant.replace("-", " ").title()
guidelines: dict[str, str] = {
"tutorial": (
"**What makes a good tutorial:**\n"
"- Has a clear learning goal\n"
"- Provides a complete, working example\n"
"- Includes all prerequisites\n"
"- Takes the reader step-by-step\n"
"- Avoids unnecessary explanation (link to Explanation docs instead)\n"
"- Ensures the reader succeeds"
),
"how-to": (
"**What makes a good how-to guide:**\n"
"- Addresses a specific, real-world problem\n"
"- Assumes the reader is competent\n"
"- Jumps straight to the steps\n"
"- Provides the solution, not the theory\n"
"- Has a clear title: 'How to...'"
),
"reference": (
"**What makes good reference documentation:**\n"
"- Is complete and accurate\n"
"- Uses tables for parameters, options, and endpoints\n"
"- Maintains a neutral, factual tone\n"
"- Mirrors the structure of the codebase\n"
"- Is kept up-to-date with code changes"
),
"explanation": (
"**What makes a good explanation:**\n"
"- Provides context and background\n"
"- Explains 'why' decisions were made\n"
"- Uses narrative prose, not lists\n"
"- Discusses alternatives and tradeoffs\n"
"- Can be read during study time (not while coding)"
),
}
return f"""# {title}s
{meta['description']}.
{guidelines.get(quadrant, '')}
## Documents
_No documents yet._
"""
# ---------------------------------------------------------------------------
# Scaffold logic
# ---------------------------------------------------------------------------
def scaffold_folders(
root: Path,
project_name: str,
dry_run: bool,
) -> list[Path]:
"""Create Diataxis folder structure with README templates."""
created: list[Path] = []
# Create root directory
if not root.exists():
if dry_run:
print(f" Would create: {root}/")
else:
root.mkdir(parents=True, exist_ok=True)
created.append(root)
# Create hub README
readme_path = root / "README.md"
if not readme_path.exists():
content = generate_hub_readme(project_name, "folders")
if dry_run:
print(f" Would create: {readme_path}")
else:
readme_path.write_text(content)
created.append(readme_path)
else:
print(f" Exists (skip): {readme_path}")
# Create quadrant directories
for quadrant, meta in QUADRANTS.items():
folder = meta["folder"]
quad_dir = root / folder
if not quad_dir.exists():
if dry_run:
print(f" Would create: {quad_dir}/")
else:
quad_dir.mkdir(parents=True, exist_ok=True)
created.append(quad_dir)
quad_readme = quad_dir / "README.md"
if not quad_readme.exists():
content = generate_quadrant_readme(quadrant)
if dry_run:
print(f" Would create: {quad_readme}")
else:
quad_readme.write_text(content)
created.append(quad_readme)
else:
print(f" Exists (skip): {quad_readme}")
return created
def scaffold_flat(
root: Path,
project_name: str,
dry_run: bool,
) -> list[Path]:
"""Create flat Diataxis layout with hub README only."""
created: list[Path] = []
# Create root directory
if not root.exists():
if dry_run:
print(f" Would create: {root}/")
else:
root.mkdir(parents=True, exist_ok=True)
created.append(root)
# Create hub README
readme_path = root / "README.md"
if not readme_path.exists():
content = generate_hub_readme(project_name, "flat")
if dry_run:
print(f" Would create: {readme_path}")
else:
readme_path.write_text(content)
created.append(readme_path)
else:
print(f" Exists (skip): {readme_path}")
return created
def create_config(base_path: Path, layout: str, root: str, dry_run: bool) -> Path | None:
"""Write .diataxis-config.json with defaults."""
config_path = base_path / ".diataxis-config.json"
if config_path.exists():
print(f" Exists (skip): {config_path}")
return None
config = dict(DEFAULT_CONFIG)
config["layout"] = layout
config["root"] = root
if dry_run:
print(f" Would create: {config_path}")
return config_path
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
f.write("\n")
return config_path
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description="Scaffold a Diataxis documentation structure"
)
parser.add_argument(
"--root",
"-r",
default=None,
help="Docs root directory (default: from config or 'docs')",
)
parser.add_argument(
"--layout",
"-l",
choices=["folders", "flat"],
default=None,
help="Layout type (default: from config or 'folders')",
)
parser.add_argument(
"--config",
"-c",
default=None,
help="Path to .diataxis-config.json",
)
parser.add_argument(
"--init-config",
action="store_true",
help="Create .diataxis-config.json with defaults",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be created without writing",
)
args = parser.parse_args()
base_path = Path.cwd()
config = resolve_config(args.config, base_path)
# Determine layout
layout = args.layout or config.get("layout", "folders")
# Determine root directory
if args.root:
root = Path(args.root)
if not root.is_absolute():
root = base_path / root
root_rel = args.root
else:
root_rel = config.get("root", "docs")
root = base_path / root_rel
# Derive project name
git_root = find_git_root(base_path)
project_name = (git_root or base_path).name.replace("-", " ").replace("_", " ").title()
# Optionally create config file
if args.init_config:
config_target = git_root or base_path
if args.dry_run:
print("Config:")
create_config(config_target, layout, root_rel, args.dry_run)
if args.dry_run:
print()
# Scaffold
mode = "(dry-run)" if args.dry_run else ""
print(f"Scaffolding Diataxis structure at: {root}/ [{layout}] {mode}")
print()
if layout == "folders":
created = scaffold_folders(root, project_name, args.dry_run)
else:
created = scaffold_flat(root, project_name, args.dry_run)
if not created:
print("\nNothing to create — structure already exists.")
else:
print(f"\n{'Would create' if args.dry_run else 'Created'}: {len(created)} items")
if not args.dry_run and created:
print(f"\nNext steps:")
print(f" 1. Add documentation to each quadrant directory")
print(f" 2. Run: uv run diataxis_audit.py --dir {root} to check coverage")
print(f" 3. Run: uv run diataxis_validate.py --dir {root} to check purity")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Validate Diataxis quadrant purity of markdown documentation.
Checks that documents maintain focus on a single Diataxis quadrant and
follow quadrant-specific best practices.
Usage:
uv run diataxis_validate.py --dir docs
uv run diataxis_validate.py --file docs/getting-started.md
uv run diataxis_validate.py --dir docs --strict
uv run diataxis_validate.py --dir docs --json
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from shared import (
ClassificationResult,
resolve_config,
scan_markdown_files,
)
from diataxis_classify import classify_file
@dataclass
class ValidationIssue:
"""A single validation finding."""
rule_id: str
severity: str # "error", "warning", "info"
file_path: Path
message: str
line: int = 0 # 0 means whole-file
suggestion: str = ""
# ---------------------------------------------------------------------------
# Validation rules
# ---------------------------------------------------------------------------
def _count_preamble_words(filepath: Path) -> int:
"""Count words before the first numbered step or code block."""
word_count = 0
in_frontmatter = False
try:
with open(filepath) as f:
for i, line in enumerate(f):
stripped = line.strip()
# Track YAML frontmatter state
if i == 0 and stripped == "---":
in_frontmatter = True
continue
if in_frontmatter:
if stripped == "---":
in_frontmatter = False
continue
# Stop at first step or code block
if re.match(r"^\d+\.\s+", stripped) or stripped.startswith("```"):
break
# Skip headings
if stripped.startswith("#"):
continue
word_count += len(stripped.split())
except (OSError, UnicodeDecodeError):
pass
return word_count
def _has_section(filepath: Path, patterns: list[str]) -> bool:
"""Check if the document has a heading matching any pattern."""
try:
with open(filepath) as f:
for line in f:
stripped = line.strip().lower()
if stripped.startswith("#"):
heading_text = re.sub(r"^#+\s*", "", stripped)
for pattern in patterns:
if re.search(pattern, heading_text):
return True
except (OSError, UnicodeDecodeError):
pass
return False
def _count_tables(filepath: Path) -> tuple[int, int]:
"""Count distinct tables and the line of the first table.
A table is a contiguous block of pipe-delimited lines.
Returns (number_of_tables, first_table_line).
"""
count = 0
first_line = 0
in_table = False
try:
with open(filepath) as f:
for i, line in enumerate(f, 1):
is_table_line = line.strip().startswith("|") and "|" in line.strip()[1:]
if is_table_line and not in_table:
# Start of a new table
in_table = True
count += 1
if first_line == 0:
first_line = i
elif not is_table_line:
in_table = False
except (OSError, UnicodeDecodeError):
pass
return count, first_line
def _count_step_instructions(filepath: Path) -> tuple[int, int]:
"""Count numbered step lines and the line of the first step."""
count = 0
first_line = 0
try:
with open(filepath) as f:
for i, line in enumerate(f, 1):
if re.match(r"^\d+\.\s+", line.strip()):
count += 1
if first_line == 0:
first_line = i
except (OSError, UnicodeDecodeError):
pass
return count, first_line
def _count_command_blocks(filepath: Path) -> tuple[int, int]:
"""Count code blocks with executable commands and the line of the first."""
count = 0
first_line = 0
in_code = False
block_start = 0
block_lines: list[str] = []
try:
with open(filepath) as f:
for i, line in enumerate(f, 1):
stripped = line.strip()
if stripped.startswith("```"):
if in_code:
# Check if block contains commands
block_text = " ".join(block_lines).lower()
if any(cmd in block_text for cmd in [
"run ", "install ", "npm ", "pip ", "docker ",
"curl ", "git ", "cd ", "mkdir ", "uv run",
]):
count += 1
if first_line == 0:
first_line = block_start
block_lines = []
else:
block_start = i
in_code = not in_code
elif in_code:
block_lines.append(stripped)
except (OSError, UnicodeDecodeError):
pass
return count, first_line
def validate_file(
filepath: Path,
classification: ClassificationResult,
) -> list[ValidationIssue]:
"""Run all validation rules against a single file."""
issues: list[ValidationIssue] = []
quadrant = classification.primary_quadrant
if not quadrant:
# DX005: No clear quadrant signal
issues.append(ValidationIssue(
rule_id="DX005",
severity="info",
file_path=filepath,
message="No clear quadrant signal detected",
suggestion="Add a clear purpose heading or rename the file to indicate its type",
))
return issues
# DX006: Collapsed document
if classification.is_collapsed:
mixed = " + ".join(q.capitalize() for q in classification.collapsed_quadrants)
issues.append(ValidationIssue(
rule_id="DX006",
severity="warning",
file_path=filepath,
message=f"Collapsed document: mixes {mixed}",
suggestion=f"Consider splitting into separate {' and '.join(classification.collapsed_quadrants)} documents",
))
# Quadrant-specific rules
if quadrant == "tutorial":
# DX001: Tutorial contains reference tables
table_count, table_line = _count_tables(filepath)
if table_count >= 3:
issues.append(ValidationIssue(
rule_id="DX001",
severity="warning",
file_path=filepath,
message=f"Tutorial contains {table_count} tables (reference-style content)",
line=table_line,
suggestion="Move parameter/option tables to a separate Reference document",
))
# DX007: Tutorial missing prerequisites
if not _has_section(filepath, [r"prerequisites?", r"before you begin", r"requirements?"]):
issues.append(ValidationIssue(
rule_id="DX007",
severity="info",
file_path=filepath,
message="Tutorial missing prerequisites section",
suggestion="Add a 'Prerequisites' or 'Before You Begin' heading",
))
# DX008: Tutorial missing learning objectives
if not _has_section(filepath, [r"what you.ll learn", r"learning objectives?", r"goals?"]):
issues.append(ValidationIssue(
rule_id="DX008",
severity="info",
file_path=filepath,
message="Tutorial missing 'What You'll Learn' section",
suggestion="Add a brief section describing what the reader will learn",
))
elif quadrant == "how-to":
# DX002: How-to with long conceptual preamble
preamble_words = _count_preamble_words(filepath)
if preamble_words > 100:
issues.append(ValidationIssue(
rule_id="DX002",
severity="warning",
file_path=filepath,
message=f"How-to guide has {preamble_words}-word preamble before first action step",
suggestion="Move conceptual content to an Explanation document; start with the task",
))
# DX009: How-to missing problem statement
if not _has_section(filepath, [r"problem", r"scenario", r"when to use", r"use case"]):
issues.append(ValidationIssue(
rule_id="DX009",
severity="info",
file_path=filepath,
message="How-to guide missing problem statement",
suggestion="Add a brief description of the problem this guide solves",
))
elif quadrant == "reference":
# DX003: Reference contains step-by-step instructions
step_count, step_line = _count_step_instructions(filepath)
if step_count >= 3:
issues.append(ValidationIssue(
rule_id="DX003",
severity="warning",
file_path=filepath,
message=f"Reference document contains {step_count} numbered steps (how-to content)",
line=step_line,
suggestion="Move step-by-step instructions to a How-to guide",
))
# DX010: Reference missing parameter/option tables
table_count, _ = _count_tables(filepath)
if table_count == 0:
issues.append(ValidationIssue(
rule_id="DX010",
severity="info",
file_path=filepath,
message="Reference document has no tables",
suggestion="Consider adding tables for parameters, options, or API endpoints",
))
elif quadrant == "explanation":
# DX004: Explanation contains execution commands
cmd_count, cmd_line = _count_command_blocks(filepath)
if cmd_count >= 2:
issues.append(ValidationIssue(
rule_id="DX004",
severity="warning",
file_path=filepath,
message=f"Explanation document contains {cmd_count} command blocks (how-to content)",
line=cmd_line,
suggestion="Move executable commands to a How-to guide; keep concepts here",
))
return issues
# ---------------------------------------------------------------------------
# Output formatters
# ---------------------------------------------------------------------------
def format_report(
issues: list[ValidationIssue],
docs_dir: Path,
file_count: int,
) -> str:
"""Format validation results as a human-readable report."""
lines = [
"Diataxis Purity Validation Report",
"=" * 40,
f"Directory: {docs_dir}",
f"Files validated: {file_count}",
"",
]
errors = [i for i in issues if i.severity == "error"]
warnings = [i for i in issues if i.severity == "warning"]
infos = [i for i in issues if i.severity == "info"]
if errors:
lines.append(f"Errors: {len(errors)}")
for issue in errors:
loc = f" (line {issue.line})" if issue.line else ""
lines.append(f" x {issue.rule_id} {issue.file_path.name}{loc} — {issue.message}")
if issue.suggestion:
lines.append(f" Suggestion: {issue.suggestion}")
lines.append("")
if warnings:
lines.append(f"Warnings: {len(warnings)}")
for issue in warnings:
loc = f" (line {issue.line})" if issue.line else ""
lines.append(f" ! {issue.rule_id} {issue.file_path.name}{loc} — {issue.message}")
if issue.suggestion:
lines.append(f" Suggestion: {issue.suggestion}")
lines.append("")
if infos:
lines.append(f"Info: {len(infos)}")
for issue in infos:
lines.append(f" - {issue.rule_id} {issue.file_path.name} — {issue.message}")
lines.append("")
if not issues:
lines.append("No issues found.")
lines.append("")
# Result
if errors:
result = "FAIL"
elif warnings:
result = "WARN"
else:
result = "PASS"
lines.append(
f"Result: {result} ({file_count} files, "
f"{len(errors)} errors, {len(warnings)} warnings, {len(infos)} info)"
)
return "\n".join(lines)
def format_issues_json(issues: list[ValidationIssue]) -> str:
"""Format validation issues as JSON."""
data = []
for issue in issues:
data.append({
"rule_id": issue.rule_id,
"severity": issue.severity,
"file": str(issue.file_path),
"message": issue.message,
"line": issue.line,
"suggestion": issue.suggestion,
})
return json.dumps(data, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate Diataxis quadrant purity of documentation"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--dir",
"-d",
default=None,
help="Docs directory to validate",
)
group.add_argument(
"--file",
"-f",
default=None,
help="Single file to validate",
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit code 1 on any warnings (for CI)",
)
parser.add_argument(
"--config",
"-c",
default=None,
help="Path to .diataxis-config.json",
)
parser.add_argument(
"--json",
action="store_true",
dest="json_output",
help="Output results as JSON",
)
args = parser.parse_args()
base_path = Path.cwd()
config = resolve_config(args.config, base_path)
# Collect files
if args.file:
filepath = Path(args.file)
if not filepath.is_absolute():
filepath = base_path / filepath
if not filepath.exists():
print(f"Error: File not found: {filepath}")
return 1
files = [filepath]
docs_dir = filepath.parent
else:
docs_dir = Path(args.dir)
if not docs_dir.is_absolute():
docs_dir = base_path / docs_dir
if not docs_dir.exists():
print(f"Error: Directory does not exist: {docs_dir}")
return 1
ignore = config.get("ignore", [])
files = scan_markdown_files(docs_dir, ignore)
if not files:
print("No markdown files found.")
return 0
# Classify and validate
all_issues: list[ValidationIssue] = []
for filepath in files:
classification = classify_file(filepath, config)
issues = validate_file(filepath, classification)
all_issues.extend(issues)
# Output
if args.json_output:
print(format_issues_json(all_issues))
else:
print(format_report(all_issues, docs_dir, len(files)))
# Exit code
errors = [i for i in all_issues if i.severity == "error"]
warnings = [i for i in all_issues if i.severity == "warning"]
if errors:
return 1
if warnings and args.strict:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Shared utilities for diataxis scripts.
Common helpers for quadrant classification, config loading, doc scanning,
and result formatting. Uses only standard library. Python 3.11+.
"""
import fnmatch
import json
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
# The four Diataxis quadrants
QUADRANTS: dict[str, dict[str, str]] = {
"tutorial": {
"folder": "tutorials",
"description": "Learning-oriented, step-by-step lessons",
"short": "Step-by-step lessons",
},
"how-to": {
"folder": "how-to",
"description": "Task-oriented, practical problem-solving guides",
"short": "Practical guides",
},
"reference": {
"folder": "reference",
"description": "Information-oriented, technical descriptions",
"short": "Technical descriptions",
},
"explanation": {
"folder": "explanation",
"description": "Understanding-oriented, conceptual discussions",
"short": "Conceptual discussions",
},
}
# Signal tables for classification — each quadrant has four signal types
QUADRANT_SIGNALS: dict[str, dict[str, list[str]]] = {
"tutorial": {
"title_keywords": [
"tutorial", "getting-started", "learn", "beginner", "introduction",
"first-steps", "walkthrough", "lesson", "workshop", "starter",
"quickstart", "quick-start", "onboarding", "crash-course",
],
"heading_patterns": [
r"step\s+\d+", r"prerequisites?", r"what you.ll learn",
r"by the end", r"let.s\b", r"learning objectives?",
r"before you begin", r"your first",
],
"content_keywords": [
"follow along", "in this tutorial", "you will learn",
"let's create", "let's build", "hands-on", "exercise",
"we will", "next step", "congratulations", "well done",
"you have successfully", "what you learned",
"in this lesson", "let's start",
],
"structural_signals": [
"numbered_steps", "prerequisite_section", "goal_statement",
"incremental_code",
],
},
"how-to": {
"title_keywords": [
"how-to", "howto", "guide", "recipe", "cookbook", "setup",
"configure", "install", "deploy", "migrate", "troubleshoot",
"fix", "resolve", "upgrade", "integrate",
],
"heading_patterns": [
r"how to\b", r"steps?:", r"procedure", r"solution",
r"workaround", r"resolution",
],
"content_keywords": [
"run the following", "execute", "to do this",
"use the command", "configure the", "set up",
"problem:", "workaround", "make sure",
"you need to", "ensure that", "run this",
"to fix this", "to resolve",
],
"structural_signals": [
"task_heading", "short_steps", "no_conceptual_intro",
"command_blocks",
],
},
"reference": {
"title_keywords": [
"reference", "api", "specification", "spec", "schema",
"config", "configuration", "options", "parameters",
"glossary", "changelog", "release-notes", "endpoints",
"commands", "flags", "env", "environment",
],
"heading_patterns": [
r"parameters?", r"options?", r"returns?", r"arguments?",
r"properties", r"fields?", r"methods?", r"endpoints?",
r"types?", r"syntax", r"flags?", r"commands?",
],
"content_keywords": [
"default:", "type:", "required", "optional", "deprecated",
"since version", "returns", "throws", "raises",
"accepted values", "valid values", "enum:",
"string | number", "boolean",
],
"structural_signals": [
"tables", "definition_lists", "code_signatures",
"parameter_tables",
],
},
"explanation": {
"title_keywords": [
"explanation", "concept", "understanding", "why",
"background", "overview", "architecture", "design",
"philosophy", "rationale", "deep-dive", "theory",
"principles", "fundamentals", "internals",
],
"heading_patterns": [
r"\bwhy\b", r"background", r"context", r"how .* works",
r"under the hood", r"design decision", r"trade-?offs?",
r"motivation", r"rationale",
],
"content_keywords": [
"the reason", "this is because", "historically",
"the philosophy", "in contrast", "on the other hand",
"the tradeoff", "consider", "fundamentally",
"the key insight", "the approach", "by design",
"the motivation", "architecturally",
],
"structural_signals": [
"long_paragraphs", "few_code_blocks", "narrative_flow",
"diagrams",
],
},
}
DEFAULT_CONFIG: dict = {
"version": 1,
"root": "docs",
"layout": "folders",
"ignore": ["node_modules", ".git", "adr", "rfcs", "*.pdf", ".DS_Store"],
"custom_signals": {},
}
@dataclass
class ClassificationResult:
"""Result of classifying a single file into a Diataxis quadrant."""
file_path: Path
primary_quadrant: str = "" # "tutorial", "how-to", "reference", "explanation"
confidence: str = "low" # high, medium, low
score: float = 0.0
scores: dict[str, float] = field(default_factory=dict)
signals: dict[str, list[str]] = field(default_factory=dict)
is_collapsed: bool = False
collapsed_quadrants: list[str] = field(default_factory=list)
reason: str = ""
def find_git_root(start: Path) -> Path | None:
"""Walk up from start to find the git repository root."""
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
cwd=start,
)
if result.returncode == 0:
return Path(result.stdout.strip())
except FileNotFoundError:
pass
return None
def load_config(base_path: Path) -> dict:
"""Find .diataxis-config.json walking up to git root, or return defaults."""
current = base_path.resolve()
git_root = find_git_root(current)
stop_at = git_root or current
while True:
config_path = current / ".diataxis-config.json"
if config_path.exists():
with open(config_path) as f:
config = json.load(f)
for key, value in DEFAULT_CONFIG.items():
if key not in config:
config[key] = value
return config
if current == stop_at or current == current.parent:
break
current = current.parent
return dict(DEFAULT_CONFIG)
def resolve_config(args_config: str | None, base_path: Path) -> dict:
"""Load config from an explicit path or auto-detect."""
if args_config:
config_path = Path(args_config)
if not config_path.is_absolute():
config_path = base_path / config_path
if config_path.exists():
with open(config_path) as f:
config = json.load(f)
for key, value in DEFAULT_CONFIG.items():
if key not in config:
config[key] = value
return config
else:
print(f"Warning: Config not found: {config_path}, using defaults",
file=sys.stderr)
return dict(DEFAULT_CONFIG)
return load_config(base_path)
def is_ignored(name: str, ignore_patterns: list[str]) -> bool:
"""Check if a directory or file name matches any ignore pattern."""
return any(
fnmatch.fnmatch(name, p) or fnmatch.fnmatch(name.lower(), p.lower())
for p in ignore_patterns
)
def find_docs_dir(base_path: Path, config: dict) -> Path:
"""Locate the docs directory from config or common conventions."""
root = config.get("root", "docs")
docs_dir = base_path / root
if docs_dir.is_dir():
return docs_dir
# Try common alternatives
for candidate in ["doc", "documentation"]:
alt = base_path / candidate
if alt.is_dir():
return alt
# Fall back to configured root even if it doesn't exist
return docs_dir
def scan_markdown_files(
docs_dir: Path,
ignore_patterns: list[str],
) -> list[Path]:
"""Recursively find all .md files, respecting ignore patterns."""
files: list[Path] = []
if not docs_dir.is_dir():
return files
for item in sorted(docs_dir.rglob("*.md")):
# Check if any parent directory is ignored
skip = False
for parent in item.relative_to(docs_dir).parents:
if parent.name and is_ignored(parent.name, ignore_patterns):
skip = True
break
if skip:
continue
if is_ignored(item.name, ignore_patterns):
continue
if item.is_file():
files.append(item)
return files
def read_document(filepath: Path, max_lines: int = 200) -> tuple[str, str, list[str]]:
"""Read a markdown document and extract structured information.
Returns:
tuple: (title, body_text, headings)
- title: first H1 heading text (lowercase)
- body_text: first max_lines of text (lowercase)
- headings: list of all heading texts (lowercase)
"""
title = ""
body_lines: list[str] = []
headings: list[str] = []
try:
with open(filepath) as f:
for i, line in enumerate(f):
if i >= max_lines:
break
stripped = line.strip()
# Extract headings
heading_match = re.match(r"^(#{1,4})\s+(.+)$", stripped)
if heading_match:
level = len(heading_match.group(1))
text = heading_match.group(2).strip().lower()
headings.append(text)
if level == 1 and not title:
title = text
body_lines.append(line.lower())
except (OSError, UnicodeDecodeError):
pass
return title, " ".join(body_lines), headings
def format_confidence(score: float) -> str:
"""Convert a numeric score to a confidence level."""
if score >= 0.7:
return "high"
elif score >= 0.4:
return "medium"
return "low"
def format_bar(value: int, total: int, width: int = 10) -> str:
"""Format a simple bar chart using Unicode block characters."""
if total == 0:
return " " * width
filled = round(value / total * width)
return "\u2588" * filled + "\u2591" * (width - filled)
Diataxis Troubleshooting
Common issues and solutions when using the Diataxis skill.
---
Script Execution Issues
uv command not found
Symptom: command not found: uv
Fix: Install uv:
curl -LsSf https://astral.sh/uv/install.sh | shOr run scripts directly with Python:
python3 scripts/diataxis_classify.py docs/*.mdPermission denied
Symptom: bash: permission denied: scripts/diataxis_classify.py
Fix:
chmod +x scripts/diataxis_classify.pyModuleNotFoundError: shared
Symptom: ModuleNotFoundError: No module named 'shared'
Fix: Run scripts from the skill directory or ensure the scripts directory contains shared.py:
ls plugins/doc/skills/diataxis/scripts/shared.py---
Classification Issues
All files classified as "low" confidence
Possible causes: 1. Files lack quadrant-specific keywords in titles or content 2. Files are very short (< 10 lines) 3. Files use non-standard naming conventions
Diagnosis:
uv run scripts/diataxis_classify.py docs/*.md --verboseCheck the per-quadrant scores. If all scores are near 0, the content may not match any keyword patterns.
Solutions:
- Rename files with clearer names (e.g.,
notes.md→how-to-deploy.md) - Add clear headings that signal the quadrant type
- Use
--no-contentto test filename-only classification
README.md always classified as "low" confidence
Expected behavior. README files are intentionally mixed-purpose documents. They typically contain elements of multiple quadrants (quick start tutorial, feature reference, installation how-to). Low confidence is correct — the README is not a pure Diataxis document.
Recommendation: Exclude README.md from Diataxis analysis or accept the low confidence. Add "README.md" to the ignore list in .diataxis-config.json.
Tutorial vs How-to misclassification
Common issue: Short tutorials classified as how-to guides, or detailed how-to guides classified as tutorials.
Key distinction:
- Tutorial = learning journey (for study, builds skills)
- How-to = task solution (for work, solves a problem)
Tips to improve classification:
- Tutorials should have: "What you'll learn", prerequisites, numbered steps that build on each other
- How-to guides should have: clear problem statement, concise steps, assumed competence
Collapsed document false positives
Symptom: A document is flagged as collapsed but should be a single document.
When this is OK:
- README files (always mixed)
- Small docs that naturally combine two types
- ADR/RFC documents (explanation + reference by nature)
When to split:
- A tutorial that includes 5+ configuration tables → extract tables to reference
- A how-to guide with 500+ words of theory → extract theory to explanation
- An architecture doc with API endpoint tables → extract endpoints to reference
---
Validation Issues
DX001: Tutorial contains reference tables
What it means: Your tutorial has tabular data (parameter lists, option tables) that belongs in a Reference document.
Fix: Extract the tables into a separate reference doc and link to it:
For the full list of options, see the Configuration Reference (`reference/config.md`).DX002: How-to has long preamble
What it means: Your how-to guide has 100+ words of text before the first actionable step. How-to guides should get to the point quickly.
Fix: Move the conceptual introduction to an Explanation document. Start the how-to with the problem statement and first step.
DX003: Reference contains steps
What it means: Your reference document has numbered step-by-step instructions. Reference docs should describe, not instruct.
Fix: Move the procedural content to a How-to guide. Keep the reference factual and tabular.
DX004: Explanation contains commands
What it means: Your explanation document has executable code blocks. Explanations should explain concepts, not show how to do things.
Fix: Move command examples to a How-to guide. Keep the explanation focused on the "why" and "how it works" conceptually.
DX006: Collapsed document
What it means: The document has strong signals for two or more quadrants (both score > 0.3 with a ratio < 2:1).
Fix: Split the document. See Example 6 in EXAMPLES.md for a step-by-step guide.
---
Audit Issues
Quality score below 50
Components to address:
- Low coverage balance: Write docs for missing quadrants
- Low quadrant purity: Split collapsed documents
- Low classification confidence: Improve file naming and headings
- Low documentation volume: Write more documentation
"No markdown files found"
Check: 1. The --dir path is correct 2. Files have .md extension 3. Files are not excluded by ignore patterns in .diataxis-config.json
---
Config Issues
Config file not found
Symptom: Warning: Config not found: .diataxis-config.json, using defaults
Not a problem. The config file is optional. All scripts work with sensible defaults. Create one with:
uv run scripts/diataxis_scaffold.py --init-configCustom ignore patterns not working
Check: The ignore array in .diataxis-config.json uses glob patterns:
{
"ignore": ["node_modules", ".git", "adr", "*.pdf", "README.md"]
}Patterns match directory and file names (not full paths).
Diataxis Workflow
Step-by-step methodology for applying the Diataxis framework to a project's documentation.
Table of Contents
1. Phase 1: Discover 2. Phase 2: Classify 3. Phase 3: Audit 4. Phase 4: Validate 5. Phase 5: Scaffold 6. Phase 6: Reorganize
---
Phase 1: Discover
Goal: Understand the current state of documentation before making changes.
Steps
1. Identify the docs directory:
ls docs/ # or doc/, documentation/2. Count and list all markdown files:
find docs -name "*.md" | wc -l3. Check if a .diataxis-config.json exists (indicates prior Diataxis setup)
4. Look for existing structure patterns:
- Are docs already in subdirectories?
- Is there a README or index?
- Are there numbered (J.D) prefixes?
Decision Points
- No docs directory: Jump to Phase 5: Scaffold
- Few docs (< 5): Skip audit, go to Phase 2: Classify
- Many docs (5+): Proceed to Phase 2: Classify
---
Phase 2: Classify
Goal: Determine which Diataxis quadrant each document belongs to.
Steps
1. Classify all markdown files:
uv run scripts/diataxis_classify.py docs/*.md --verbose2. Review the results table. For each file, check:
- Is the quadrant assignment correct?
- Is the confidence level acceptable?
- Are any files flagged as "collapsed"?
3. For low-confidence files, read the document and decide manually:
- Does it teach through guided practice? → Tutorial
- Does it solve a specific task? → How-to
- Does it describe system facts? → Reference
- Does it explain concepts or decisions? → Explanation
4. For collapsed documents, decide:
- Split: Separate into multiple focused documents
- Accept: Some docs legitimately serve dual purposes (e.g., README)
Distinguishing Tutorial vs How-to
This is the hardest classification distinction. Key differentiators:
| Signal | Tutorial | How-to |
|---|---|---|
| Length | Long, complete journey | Short, focused task |
| Starts with | "What you'll learn" / prerequisites | Problem statement / "You need to..." |
| Steps | Sequential, all required | May have alternatives |
| Context | Provides full setup | Assumes existing project |
| Tone | "Let's create..." | "Run the following..." |
| Code | Builds up incrementally | Complete solutions |
JSON Output
For scripting or integration:
uv run scripts/diataxis_classify.py docs/*.md --json > classification.json---
Phase 3: Audit
Goal: Assess documentation coverage across all four quadrants.
Steps
1. Run the audit:
uv run scripts/diataxis_audit.py --dir docs2. Review the coverage report:
- Quadrant distribution: Are all four quadrants represented?
- Coverage gaps: Which quadrants are missing or underrepresented?
- Collapsed documents: Which docs need splitting?
- Quality score: Target 70+ for mature projects
3. Address gaps by planning new documents:
| Missing Quadrant | What to Write |
|---|---|
| Tutorial | Getting-started guide, first project walkthrough |
| How-to | Task-specific guides (deploy, configure, migrate) |
| Reference | API docs, config options, CLI reference |
| Explanation | Architecture overview, design rationale |
Quality Score Breakdown
The quality score (0-100) has four components:
- Coverage balance (25): How evenly docs are distributed across quadrants
- Quadrant purity (25): Penalty for collapsed documents
- Classification confidence (25): Average confidence of classifications
- Documentation volume (25): Total documentation depth
---
Phase 4: Validate
Goal: Check that documents maintain quadrant purity — each doc focuses on one type.
Steps
1. Run validation:
uv run scripts/diataxis_validate.py --dir docs2. Review warnings:
- DX001-DX004: Quadrant mixing — content from one quadrant appears in another
- DX005-DX006: Classification issues — unclear or collapsed documents
- DX007-DX010: Best practice suggestions — missing recommended sections
3. For each warning, decide:
- Fix: Move the mixed content to the correct quadrant document
- Accept: Some warnings are acceptable (e.g., a README with both setup steps and config reference)
CI Integration
Use --strict to fail CI on warnings:
uv run scripts/diataxis_validate.py --dir docs --strictSingle File Validation
Validate a specific file:
uv run scripts/diataxis_validate.py --file docs/getting-started.md---
Phase 5: Scaffold
Goal: Create a Diataxis-aware folder structure for new or existing projects.
Steps
1. Preview the scaffold:
uv run scripts/diataxis_scaffold.py --dry-run2. Choose a layout:
- Folders (default, recommended for 10+ docs):
uv run scripts/diataxis_scaffold.py --layout foldersCreates: tutorials/, how-to/, reference/, explanation/
- Flat (for smaller projects):
uv run scripts/diataxis_scaffold.py --layout flatCreates: Hub README with quadrant sections
3. Optionally create a config file:
uv run scripts/diataxis_scaffold.py --init-configFolder Layout Output
docs/
├── README.md # Hub page with quadrant overview
├── tutorials/
│ └── README.md # Guidelines for writing tutorials
├── how-to/
│ └── README.md # Guidelines for writing how-to guides
├── reference/
│ └── README.md # Guidelines for writing reference docs
└── explanation/
└── README.md # Guidelines for writing explanations---
Phase 6: Reorganize
Goal: Move existing documents into the Diataxis structure.
Steps
1. After scaffolding and classifying, move documents:
# Move tutorials
mv docs/getting-started.md docs/tutorials/
mv docs/first-project.md docs/tutorials/
# Move how-to guides
mv docs/deploy-guide.md docs/how-to/
mv docs/configure-auth.md docs/how-to/
# Move reference docs
mv docs/api-reference.md docs/reference/
mv docs/config-options.md docs/reference/
# Move explanations
mv docs/architecture.md docs/explanation/
mv docs/design-decisions.md docs/explanation/2. Handle collapsed documents:
- Read the document and identify the mixed sections
- Extract reference tables →
reference/ - Extract conceptual sections →
explanation/ - Keep the core content in the appropriate quadrant
3. Update cross-references:
# Find all references to moved files
grep -r "getting-started.md" docs/4. Re-run audit to verify improvements:
uv run scripts/diataxis_audit.py --dir docs---
Coordination with Other Skills
With doc-coauthoring
After classifying a document's quadrant, suggest the appropriate writing style:
- Tutorial: Use doc-coauthoring's Full Collaborative workflow with encouraging tone
- How-to: Use Streamlined workflow, recipe-style
- Reference: Use Streamlined workflow with table-heavy patterns
- Explanation: Use Full Collaborative workflow with narrative prose
With jd-docs
The Diataxis skill operates independently of Johnny.Decimal structure. If both are active:
- Diataxis classifies by content type (what kind of doc)
- J.D classifies by topic area (where it belongs organizationally)
- A doc can be in
20-architecture/(J.D area) and classified as "explanation" (Diataxis quadrant)