Documentation Health Metrics
How Frontend Engineering measures its own quality. At 1000+ documents, "it's good" is not auditable and "a reviewer checked it" does not scale. This document defines measurable health metrics — each with an explicit scoring rule — so the state of the knowledge base is a number a maintainer can track, a report CI can generate, and a target a contributor can hit.
The metrics exist to surface problems, not to game. A high score is necessary, never sufficient; a human still owns correctness. Use them to find the weakest articles and the drifting trends, not to rubber-stamp.
Table of contents
- How scoring works
- Completeness score
- Accuracy score
- Readability score
- Cross-link coverage
- Reference coverage
- Freshness score
- Code quality score
- Maintenance score
- The composite health score
- Repository-level metrics
- Known tooling gaps
How scoring works
- Each metric is 0–100. Higher is better. Most are computed per article; some are repository-level.
- Two kinds of check: automatable (a script can compute it — structure, links, frontmatter, freshness, lint) and judged (a reviewer scores it — accuracy, readability, trade-off honesty). Automatable metrics run in CI; judged metrics are set at review and stored.
- A publishable article needs ≥ 85 on every metric and passes all hard gates regardless of score. Scores below threshold block or flag.
- Scores are recorded (in the article's review record / a generated report), so trends are visible over time, not just at a point.
Completeness score
Automatable. Is every required part present and non-empty?
- What it measures: presence and substance of the mandatory sections and all required frontmatter fields.
- Scoring: start at 100. Subtract 8 per missing required section; subtract 4 per section that is present but empty or a single filler sentence where content is expected (a legitimate one-line "not applicable" does not count as empty); subtract 5 per missing required frontmatter field; subtract 10 if the frontmatter does not mirror
graph.json. - Threshold: 100 to publish — completeness is binary in spirit. Anything less means a section is missing.
- Source: extends
scripts/validate-frontmatter.py; a section-presence check should be added (see known gaps).
Accuracy score
Judged, at technical review and fact check. Is it true?
- What it measures: correctness of claims, honesty and symmetry of trade-offs, validity of the mental model, correct version pinning, and citation integrity.
- Scoring rubric (reviewer assigns):
- 100–90: every claim verified against a primary source; trade-offs symmetric; recommendation derivable; versions correct.
- 89–70: minor unverified claims or a thin disadvantages section; no factual errors.
- 69–50: a claim could not be verified, or the trade-off analysis is one-sided.
- < 50: a factual error, a fabricated citation, or a strawman alternative — blocks publish.
- Threshold: ≥ 90 to publish. Accuracy is the metric the fact-check gate defends; it never ships below 90.
Readability score
Mostly automatable, reviewer-adjusted. Can the target reader follow it efficiently?
- What it measures: sentence and paragraph length, presence of the front-loaded TL;DR, heading structure, absence of banned marketing words, prose-over-slides balance.
- Scoring: start at 100. Subtract 3 per banned word; subtract 5 if average sentence length exceeds ~28 words or paragraphs routinely exceed ~5 sentences; subtract 5 if reasoning is delivered as bullet lists instead of prose; subtract 5 for missing/weak TL;DR; subtract 3 per heading-hierarchy violation. A reviewer may adjust ±10 for clarity a script can't see.
- Calibrated to the audience (mid-to-senior engineers) — not a general-reader grade level. Precision beats simplicity.
- Threshold: ≥ 85.
Cross-link coverage
Automatable. Is the article properly wired into the graph?
- What it measures: presence and validity of the five typed relations and their two-way integrity.
- Scoring: start at 100. Subtract 10 per required relation array that is absent (not
[]— absent); subtract 15 per link that does not resolve to an existing node; subtract 10 ifalternativesis non-symmetric; subtract 10 if the prerequisite graph would become cyclic; subtract 5 ifnextis hand-edited out of sync with the derived inverse. An honest emptyalternatives: []is not penalized. - Threshold: ≥ 85, and zero unresolved links (that's a hard gate too).
- Source:
scripts/validate-links.pyalready enforces the invariants; this metric turns its pass/fail into a graded coverage number.
Reference coverage
Automatable presence + judged quality. Are claims backed by sources?
- What it measures: whether the article cites primary sources, whether the links resolve, and (judged) whether the references are primary and actually support their claims.
- Scoring: start at 100. Subtract 20 if the References section is empty on an article making external claims; subtract 10 per dead reference link; subtract 5 per reference that is a secondary source where a primary one exists; subtract 10 (reviewer, at fact check) if a cited source does not support the sentence it backs.
- Self-containment caveat: the article must stand without the references — so "few references" is fine for a purely reasoning-based article; "unsupported external claims" is not.
- Threshold: ≥ 85; zero dead links (hard gate via the dead-link workflow).
Freshness score
Automatable. Has it been reviewed within its window?
- What it measures: age of
last_reviewedagainst the article's volatility window (High 6mo / Medium 12mo / Low 24mo). - Scoring: 100 if reviewed within the window; decays linearly to 50 at 2× the window; < 50 (flagged overdue) beyond that. A
Deprecatedarticle with a current banner scores as fresh; anArchivedarticle is excluded. - Threshold: ≥ 85 keeps an article in good standing; below that it enters the maintenance queue. Freshness is the one metric that decays on its own — an untouched article's score falls with time, which is the point.
- Source: computed from
last_reviewed+ inferred/curated volatility; drives the repository freshness report.
Code quality score
Automatable + judged. Do the examples meet the bar?
- What it measures: the Good Example at Level A and the Bad Example's realism, per
code-example-standard.md. - Scoring: start at 100. Subtract 20 if the Good Example does not type-check under strict TypeScript; subtract 15 for missing error handling/cleanup where it matters; subtract 10 for an untagged fence; subtract 10 per
any/@ts-ignorein a Good Example; subtract 10 for a UI example with no accessibility; subtract 10 for a speculative/unjustified abstraction; subtract 15 (reviewer) if the Bad Example is a strawman. - Threshold: ≥ 90 — examples are the repository's proof of work and hold a high bar.
- Source: lint/type-check in CI where snippets are extractable; reviewer for realism and a11y.
Maintenance score
Automatable, repository- and article-level. How maintainable is this in the long run?
- What it measures: signals that predict future cost: number of hard-coded version pins, count of inbound links (blast radius of a change), whether it has a deprecation/migration path when needed, open unresolved content-correction issues against it, and time-since-last-touch relative to inbound traffic.
- Scoring: start at 100. Subtract 3 per version pin beyond the ones behavior genuinely requires; subtract 10 if a
Deprecatedarticle lacks a replacement link; subtract 5 per open unaddressed content-correction issue older than one review cycle; subtract 5 if the article is a high-inbound-degree node past its freshness window (its staleness affects many others). - Threshold: ≥ 85. A low maintenance score is an early warning, not a publish blocker on its own.
The composite health score
A single number per article, for triage — the minimum of the eight, not the average:
health = min(completeness, accuracy, readability, cross_link,
reference, freshness, code_quality, maintenance)We take the minimum deliberately: an article is only as healthy as its weakest dimension. A brilliantly written, well-linked article with a fabricated claim is not "92 average" — it is broken, and the minimum says so. Triage worst-first by composite score.
Repository-level metrics
Beyond per-article scores, track the health of the whole:
- Coverage: published articles ÷ planned articles in the inventory, per Part — where are the gaps?
- Overdue rate: share of published articles past their freshness window — is maintenance keeping up?
- Graph integrity: unresolved links, cycles, orphan nodes (target: zero, enforced by CI).
- Deprecation debt: count of
Deprecatedarticles without migration guides. - Correction latency: median time from a content-correction issue to a fix.
- Score distribution: the histogram of composite scores — a healthy repository has a tight, high distribution, not a high average hiding a long tail.
Known tooling gaps
The metrics above describe the target system; some checks are enforced today and some need building. Tracked honestly here so the gap is visible:
- Section-presence check — extend
validate-frontmatter.py(or add a script) to verify every mandatory section heading is present, feeding the Completeness score. reading_timevsreading_time_min— the current validator requires a key literally namedreading_timewhile the schema and template usereading_time_min; reconcile the validator toreading_time_min(noted inmetadata-schema.md).- Snippet extraction + type-check in CI — compile fenced
ts/tsxblocks under strict TypeScript so the Code Quality score is automated, not just reviewer-judged. - Freshness report — a scheduled job that computes freshness from
last_reviewed+ volatility and lists overdue articles. - Banned-word linter — a check that flags the marketing register automatically, feeding Readability.
Until each is automated, the corresponding metric is scored at review by a human against the rules above — the standard is the same; only the enforcement mechanism differs.
Next: review-process.md — where judged scores are set · evergreen-policy.md — the freshness window · article-quality.md — the bar the scores measure against.