
Whitepaper Audit
- 24 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
Audit a markdown white paper against a best-practices checklist with deterministic script checks plus an LLM-judge review, producing a prioritized P0-P2 report.
About
Runs two lanes over a document (script checks for readability, acronyms, structure, links; an LLM judge for overclaims, inconsistent numbers, and audience fit) and merges them into a prioritized findings report. A developer uses it to QA a technical white paper before publishing, optionally applying fixes.
- Two lanes: deterministic checks plus a fresh-context LLM judge
- Prioritized P0-P2 report; applies fixes only on explicit request
Whitepaper Audit by the numbers
- 24 all-time installs (skills.sh)
- Ranked #973 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/glebis/claude-skills --skill whitepaper-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
What it does
Audit a markdown white paper against a best-practices checklist with deterministic script checks plus an LLM-judge review, producing a prioritized P0-P2 report.
Files
whitepaper-audit
Audit a markdown white paper in two lanes and produce one merged, prioritized report.
Inputs
- Document path (required) — markdown source, not PDF.
- Stated audience (ask if not given) — severity of
audience-fit/jargon-undefined
depends on it. Default: "technical practitioners, non-academic".
- Mode —
recommend(default) orfix(only on explicit request).
Workflow
1. Lane 1 — deterministic
python3 scripts/check_doc.py <doc.md> --offline [--target-grade N] [--allow ACRO]Drop --offline to also check http(s) links (HEAD→GET, timeouts; only broken is a finding). Output: JSON findings, schema in DESIGN.md.
2. Lane 2 — LLM judge
Dispatch a subagent (fresh context — never judge a document you wrote in the same context) with references/audit-prompt.md, filling {PATH} and {AUDIENCE}, plus the [judge] criteria from references/checklist.md. The judge returns JSON findings.
Judge calibration rules are binding: verbatim quotes required; no P0 at low confidence; "needs verification", never "factually wrong".
3. Merge
Dedupe by (location, issue type) keeping both lane attributions; sort P0 → P1 → P2, then confidence. Cross-reference: a lane-1 broken link that supports a claim (judge decides materiality) is P1; decorative → P2.
4. Report (default mode)
Write a markdown report: summary verdict, findings table (id, severity, confidence, location, fix), then details. Recommend; do not edit.
5. Fix mode (only when explicitly requested)
Apply fixes P0-first. Any change to code goes through superpowers test-driven-development (test first, watch it fail). Prose fixes: edit, then re-run the full audit and report cleared vs remaining findings.
Evals
Before trusting a new/changed judge prompt, run evals/README.md procedure (planted defects + clean control; pass criteria inside). Lane 1 is covered by scripts/tests/test_check_doc.py (pytest).
Files
scripts/check_doc.py— lane 1 (stdlib-only;--helpfor flags)references/checklist.md— operational criteria, both lanesreferences/audit-prompt.md— judge prompt templateevals/— judge validation cases + pass criteriaDESIGN.md— architecture decisions (v0.2, Codex-audited)
whitepaper-audit — design spec (v0.2, post-Codex-audit)
Date: 2026-06-03 · Author: Gleb Kalinin + Claude · Codex audit: incorporated (v0.1 → v0.2)
Purpose
A general-purpose skill that audits a technical/scientific white paper (or any long-form explanatory document) against a research-grounded best-practices checklist, and either (a) produces a prioritized P0–P2 findings report (default), or (b) applies corrections on request. First target: ~/ai_projects/confide/docs/WHITEPAPER.md.
Architecture
whitepaper-audit/
├── SKILL.md # triggers, workflow, modes
├── DESIGN.md # this file
├── references/
│ ├── checklist.md # operational rules w/ examples+counterexamples, tagged [script]/[judge]
│ └── audit-prompt.md # LLM-judge prompt template
├── scripts/
│ ├── check_doc.py # deterministic checks (stdlib-only)
│ └── tests/test_check_doc.py # pytest, built TDD
└── evals/
├── README.md # pass criteria, runner procedure
└── cases/ # planted-defect docs + clean + near-clean controlsFinding schema (both lanes — the merge contract)
Every finding, from either lane:
{
"check_id": "acronym-undefined", // stable id from checklist.md
"lane": "script" | "judge",
"severity": "P0" | "P1" | "P2",
"confidence": "high" | "medium" | "low",
"location": "§5.2 / quoted line",
"evidence_quote": "…verbatim excerpt…",
"rationale": "why this is a problem",
"suggested_fix": "concrete edit"
}Merged report: dedupe by (location, issue type), keep both lane attributions when merged, sort P0→P2 then confidence. Severity = impact × confidence — a low-confidence high-impact issue is reported at the lower severity with a "verify" note, never silently promoted.
Two-lane audit
Lane 1 — deterministic (check_doc.py, TDD)
Stdlib-only, Python 3.10+. JSON findings on stdout, exit 0; never crashes on malformed markdown. Preprocessing applies to all checks: strip code blocks, tables, URLs, headings, footnote markers, and HTML comments before text metrics.
| check_id | Method | Severity gating |
|---|---|---|
readability | Flesch-Kincaid grade per section (heuristic syllable counter — trend-level, documented as such) + top-5 hardest sentences | P2 by default; P1 only when an audience-critical section (abstract, intro) exceeds target by >3 grades. Default target ≤ 13. |
acronym-undefined | Acronyms only (2–6 capitals), with allowlist (units, common: PDF, URL, USA…) and config term-map. Defined = Full Term (ABC) / ABC (Full Term) / glossary entry, at or before first use. Plural/possessive normalized. | P1, high confidence |
structure | Required blocks present: title, abstract/exec summary, date/version, author, limitations, glossary (configurable set) | Missing limitations → P0 candidate (judge confirms materiality); missing glossary → P2 if terms otherwise defined; others P1 |
links | Relative paths: exist on disk. http(s): HEAD → small-GET fallback, timeout; classify broken / unreachable / skipped / uncertain — only broken is a finding; --offline skips network | P1 if supporting a claim (judge cross-references), else P2 |
Broader jargon (bolded terms, lowercase terms of art) is not lane-1: the judge flags "possible undefined jargon" as a soft finding.
Lane 2 — LLM judge (audit-prompt.md)
Judged criteria (refined by the best-practices research): apparent overclaim / unsupported claim / internally inconsistent number (NOT "factual error" — the judge has no retrieval; it flags "needs verification", it does not adjudicate truth); marketing language in a technical claim; buried lede; honesty about limitations and uncertainty; audience fit; flow; table/figure clarity; possible undefined jargon (soft).
Judge outputs findings in the shared schema, with confidence mandatory.
Severity rubric (impact × confidence, with per-check examples)
- P0 — trust-breaking: apparent overclaim stated as fact; internally inconsistent
number (says 0.88 in §5, 0.78 in abstract); missing/dishonest limitations in a doc that makes empirical claims. Example: "our tool guarantees GDPR compliance" with no evidence → P0. Counterexample: a hedge that's merely wordy → P2.
- P1 — comprehension-breaking: acronym undefined at first use; buried lede (key
finding absent from abstract); broken link that supports a claim; missing structural block a practitioner needs.
- P2 — polish: tone, wordiness, dead footer link, high FK in a non-critical section,
table formatting.
Modes
1. Default (recommend): lane 1 + lane 2 → merged report (schema above). No edits. 2. Fix (explicit request): apply corrections P0-first. Code changes (audited repo's scripts or this skill's own) go through superpowers TDD. Re-run full audit after; report cleared vs remaining.
Evals — three suites, explicit pass criteria
1. Lane-1 evals = the pytest suite (planted structural defects in fixture docs; exact expected findings). 2. Lane-2 evals: planted-defect docs (≥1 apparent overclaim, ≥1 internally inconsistent number, ≥1 jargon cluster, ≥1 missing-limitations doc) + 1 clean control + 1 near-clean control (minor P2-only flaws). Pass criteria: 100% recall of planted P0s at exact severity, location matched to the correct section; thresholded recall ≥80% for planted P1/P2; false-positive budget on controls: zero P0, ≤1 P1, ≤3 P2 per control doc. 3. Merged-output eval: one doc with defects spanning both lanes; verify dedupe and schema integrity.
Out of scope (v1)
- Grammar/spell checking; PDF auditing (markdown source only); auto-publishing/CI.
- Citation verity (whether a cited source supports the claim): judge marks "needs
verification"; a future evidence-verification lane may fetch sources.
Resolved design questions (per Codex audit)
1. Term detector → acronyms-only in v1, allowlist + exact patterns; jargon → judge. 2. Readability → yes, sentence-level: section averages for overview, top-5 hardest sentences for actionable fixes. 3. Eval threshold → all planted P0s at exact severity; thresholded P1/P2; FP budget on clean + near-clean controls (no flat 80%).
GreenCache: Measuring a Memory Cache for Weather Data
Version 1.1 · 2026-03-20 · Kim Researcher · kim@example.org
Summary
GreenCache is a small in-memory cache for weather-station readings. On a replay of 30 days of real station traffic, it answered 72% of queries from memory (95% CI 68–76%), cutting database reads by roughly two thirds. The cache helps most for stations queried many times per hour; it adds little for rarely-queried stations. This paper describes the design, the measurement method, and the limits of what we can conclude.
The problem
Weather dashboards ask for the same recent readings again and again. Each repeated question normally goes to the database, which is slow and costly. A cache — a small store of recently used answers kept in fast memory — can answer repeats directly.
Design
GreenCache keeps the most recently requested readings in memory (an LRU policy — Least Recently Used, meaning the reading unused for the longest time is evicted first when space runs out). Cache size is configurable; we tested 512 MB.
Method
We replayed 30 days of recorded query traffic (4.1 million queries) against GreenCache and counted hits and misses. The replay preserves the original timing and ordering. We report the hit rate with a bootstrap confidence interval (resampling days, 2,000 times).
Results
| Metric | Value | 95% CI |
|---|---|---|
| Hit rate | 0.72 | 0.68–0.76 |
| DB read reduction | 67% | 62–71% |
Limitations
One month of traffic from one deployment may not represent other seasons or sites. The replay ignores cache warm-up on day one (under 2% of queries). We did not measure tail latency, only hit rate. Results are specific to the 512 MB configuration.
Conclusion
For dashboards with repeated queries, a small LRU cache is a cheap, measurable win. To adopt: start with the default configuration, replay a week of your own traffic, and check your hit rate before rollout. Code and replay scripts: github.com/example/greencache.
Glossary
| Term | Meaning |
|---|---|
| Cache | A small store of recently used answers kept in fast memory. |
| Hit rate | The fraction of queries answered from the cache. |
| LRU | Least Recently Used — the eviction policy described above. |
QuickSort-NG: Faster Sorting for Log Pipelines
Version 0.9 · 2026-04-12 · Ada Developer
Summary
QuickSort-NG is a sorting library tuned for log-pipeline workloads. On our test set of 40 log files it sorted 2.3× faster than the standard library sort (median; range 1.8–2.9×). This paper explains the technique and the measurement.
The problem
Log pipelines sort huge, nearly-sorted files. General-purpose sorts waste work on data that is already mostly ordered.
How it works
QuickSort-NG detects pre-sorted runs (stretches of already-ordered records) and merges them directly, falling back to standard quicksort for disordered segments. Run detection is a single linear pass.
Results
| Workload | Speedup |
|---|---|
| Nearly sorted (90% ordered) | 2.9× |
| Half sorted | 2.2× |
| Random | 1.1× |
The gains concentrate exactly where the workload hypothesis predicts: pre-ordered data.
Conclusion
For log pipelines specifically, run-detection sorting is worth adopting. Random-data workloads should keep the standard sort.
<!-- PLANTED: limitations-present — this document makes empirical performance claims (2.3x median across 40 files) but has NO limitations section at all: nothing on test-set composition bias, hardware, memory trade-offs, or generality. expected: check_id=limitations-present, severity=P0 -->
Glossary
| Term | Meaning |
|---|---|
| Run | A stretch of records that is already in sorted order. |
VectorVault — A Revolutionary Approach to Database Caching
Version 1.0 · 2026-05-01 · Sam Engineer
Executive summary
VectorVault is a caching layer for vector databases. In our benchmark of 12 workloads, it reduced median query latency by 41% (CI 35–47%). This paper describes the design and the measurements.
The problem
Vector databases recompute similarity searches that repeat often. Caching can help, but existing caches show poor hit rates on embedding workloads.
Design
VectorVault sits between the application and the database. Our LSH-based bucketing with HNSW fallback and PQ compaction over the IVF index achieves sublinear probe amortization across the recall-bounded ANN frontier.
<!-- PLANTED: jargon-undefined — the sentence above is an unglossed jargon cluster (LSH, HNSW, PQ, IVF, ANN all undefined; audience is general practitioners). expected: check_id=jargon-undefined OR acronym-undefined, severity=P1 -->
Results
Across 12 workloads the median latency reduction was 38%, with hit rates between 0.61 and 0.84.
<!-- PLANTED: inconsistent-number — exec summary says 41%, here 38%, same quantity. expected: check_id=inconsistent-number, severity=P0 -->
VectorVault eliminates cache-miss latency entirely and guarantees that no production workload will ever regress.
<!-- PLANTED: unsupported-claim/overclaim stated as fact — "eliminates entirely", "guarantees ... ever" with no supporting evidence. expected: check_id=unsupported-claim (or marketing-tone), severity=P0 -->
Limitations
Benchmarks are synthetic; production traces may differ. N=12 workloads is small; CIs are wide. We did not test distributed deployments.
Glossary
| Term | Meaning |
|---|---|
| Vector database | A database that searches by semantic similarity rather than exact match. |
| Hit rate | The fraction of queries answered from the cache. |
Evals for whitepaper-audit (lane 2 — the LLM judge)
Lane 1 (deterministic) is validated by the pytest suite in scripts/tests/. Lane 2 validates the judge (audit-prompt.md) against planted defects, mirroring the planted-signal gold-standard approach: you only trust a reviewer you've tested against known answers.
Cases
| File | Planted defects (documented as HTML comments in-file) |
|---|---|
cases/planted-defects.md | inconsistent-number (P0) · unsupported-claim overclaim (P0) · jargon-undefined cluster (P1) |
cases/missing-limitations.md | limitations-present (P0) — empirical claims, no limitations section |
cases/clean-control.md | none — well-hedged, defined terms, CIs, limitations, glossary |
Procedure
1. For each case, give the judge the audit prompt (references/audit-prompt.md) with the case file as {PATH} and audience "general technical practitioners". Strip the HTML comments first (sed '/<!--/,/-->/d') so the judge cannot read the answer key. 2. Compare the judge's JSON findings to the planted defects.
Pass criteria (from DESIGN.md v0.2; per Codex audit)
- 100% recall of planted P0s at exact severity, location matched to the correct
section. (unsupported-claim vs marketing-tone is an accepted check_id alias for the overclaim plant; jargon-undefined vs acronym-undefined likewise.)
- Planted P1/P2: recall ≥ 80%.
- False-positive budget on the clean control: zero P0, ≤1 P1, ≤3 P2.
- Judge output must validate against the finding schema (all required keys, verbatim
evidence quotes that actually appear in the document).
Record results per run in evals/RESULTS.md (date, model, pass/fail per criterion). Re-run after any change to audit-prompt.md or the checklist.
Eval results
2026-06-03 · judge = Claude Opus 4.8 subagents (fresh context) · prompt v1
| Criterion | Result |
|---|---|
planted-defects.md: inconsistent-number P0 | ✅ found, exact severity, correct location |
planted-defects.md: unsupported-claim P0 | ✅ found, exact severity, correct location |
planted-defects.md: jargon-undefined P1 | ✅ found (medium confidence, correct location) |
missing-limitations.md: limitations-present P0 | ✅ found, exact severity |
| clean-control.md FP budget (0 P0, ≤1 P1, ≤3 P2) | ✅ zero findings of any severity |
| Schema validity (all findings) | ✅ verbatim quotes verified |
Overall: PASS (100% planted-P0 recall at exact severity; planted P1 found; clean control clean).
Notes
- The judge additionally flagged the planted doc's title ("Revolutionary") as
marketing-tone P0 — this was a deliberate part of the overclaim plant and is a correct catch, not an FP.
- The judge found genuine, unintended defects in
missing-limitations.md: the
summary's "range 1.8–2.9×" contradicts the table's 1.1× row, and the stated median 2.3× doesn't match the table median 2.2×. These are now accepted as additional plants (they are real inconsistent-number defects); future runs should expect them. Evidence of judge sharpness: it caught defects the eval author didn't notice planting.
- Lane-1 FP profile observed on a real document (CONFIDE white paper): caps taxonomy
labels in tables (EMAIL/DATE/ORG…), caps emphasis (NOT), and prose-defined metrics (F1, AUC — glossed in text but not in a parenthetical pattern). Candidate v1.1 refinement: dictionary-word filter + prose-definition pattern (ABC is/means …).
GreenCache: Measuring a Memory Cache for Weather Data
Version 1.1 · 2026-03-20 · Kim Researcher · kim@example.org
Summary
GreenCache is a small in-memory cache for weather-station readings. On a replay of 30 days of real station traffic, it answered 72% of queries from memory (95% CI 68–76%), cutting database reads by roughly two thirds. The cache helps most for stations queried many times per hour; it adds little for rarely-queried stations. This paper describes the design, the measurement method, and the limits of what we can conclude.
The problem
Weather dashboards ask for the same recent readings again and again. Each repeated question normally goes to the database, which is slow and costly. A cache — a small store of recently used answers kept in fast memory — can answer repeats directly.
Design
GreenCache keeps the most recently requested readings in memory (an LRU policy — Least Recently Used, meaning the reading unused for the longest time is evicted first when space runs out). Cache size is configurable; we tested 512 MB.
Method
We replayed 30 days of recorded query traffic (4.1 million queries) against GreenCache and counted hits and misses. The replay preserves the original timing and ordering. We report the hit rate with a bootstrap confidence interval (resampling days, 2,000 times).
Results
| Metric | Value | 95% CI |
|---|---|---|
| Hit rate | 0.72 | 0.68–0.76 |
| DB read reduction | 67% | 62–71% |
Limitations
One month of traffic from one deployment may not represent other seasons or sites. The replay ignores cache warm-up on day one (under 2% of queries). We did not measure tail latency, only hit rate. Results are specific to the 512 MB configuration.
Conclusion
For dashboards with repeated queries, a small LRU cache is a cheap, measurable win. To adopt: start with the default configuration, replay a week of your own traffic, and check your hit rate before rollout. Code and replay scripts: github.com/example/greencache.
Glossary
| Term | Meaning |
|---|---|
| Cache | A small store of recently used answers kept in fast memory. |
| Hit rate | The fraction of queries answered from the cache. |
| LRU | Least Recently Used — the eviction policy described above. |
QuickSort-NG: Faster Sorting for Log Pipelines
Version 0.9 · 2026-04-12 · Ada Developer
Summary
QuickSort-NG is a sorting library tuned for log-pipeline workloads. On our test set of 40 log files it sorted 2.3× faster than the standard library sort (median; range 1.8–2.9×). This paper explains the technique and the measurement.
The problem
Log pipelines sort huge, nearly-sorted files. General-purpose sorts waste work on data that is already mostly ordered.
How it works
QuickSort-NG detects pre-sorted runs (stretches of already-ordered records) and merges them directly, falling back to standard quicksort for disordered segments. Run detection is a single linear pass.
Results
| Workload | Speedup |
|---|---|
| Nearly sorted (90% ordered) | 2.9× |
| Half sorted | 2.2× |
| Random | 1.1× |
The gains concentrate exactly where the workload hypothesis predicts: pre-ordered data.
Conclusion
For log pipelines specifically, run-detection sorting is worth adopting. Random-data workloads should keep the standard sort.
Glossary
| Term | Meaning |
|---|---|
| Run | A stretch of records that is already in sorted order. |
VectorVault — A Revolutionary Approach to Database Caching
Version 1.0 · 2026-05-01 · Sam Engineer
Executive summary
VectorVault is a caching layer for vector databases. In our benchmark of 12 workloads, it reduced median query latency by 41% (CI 35–47%). This paper describes the design and the measurements.
The problem
Vector databases recompute similarity searches that repeat often. Caching can help, but existing caches show poor hit rates on embedding workloads.
Design
VectorVault sits between the application and the database. Our LSH-based bucketing with HNSW fallback and PQ compaction over the IVF index achieves sublinear probe amortization across the recall-bounded ANN frontier.
Results
Across 12 workloads the median latency reduction was 38%, with hit rates between 0.61 and 0.84.
VectorVault eliminates cache-miss latency entirely and guarantees that no production workload will ever regress.
Limitations
Benchmarks are synthetic; production traces may differ. N=12 workloads is small; CIs are wide. We did not test distributed deployments.
Glossary
| Term | Meaning |
|---|---|
| Vector database | A database that searches by semantic similarity rather than exact match. |
| Hit rate | The fraction of queries answered from the cache. |
LLM-judge audit prompt (v1)
Use this prompt (filling the placeholders) for the judge lane. Run it on the full document text. The deterministic lane runs separately (scripts/check_doc.py); do not duplicate its checks except where the checklist marks a criterion as both-lane.
---
You are a critical peer reviewer of white papers — rigorous, concrete, and calibrated. You review against an explicit checklist; you do not invent criteria, and you do not adjudicate external facts.
Document under review: {PATH} Stated audience: {AUDIENCE — e.g. "practitioners/clinicians; readable by a final-year school student"} Checklist: the [judge]-tagged criteria in checklist.md (provided below / alongside).
Your epistemic limits (binding):
- You have NO retrieval. You may flag a claim as *apparently unsupported within the
document, internally inconsistent, or needs verification* — you may NOT call anything a factual error about the outside world.
- Quote evidence verbatim. A finding without a verbatim quote and location is invalid.
- Calibrate: every finding carries
confidence(high/medium/low). When confidence is low
on a high-impact issue, set the severity one level lower and say "verify" in the rationale. Never report a P0 at low confidence.
Severity rubric (impact × confidence):
- P0 (trust-breaking): apparent overclaim stated as fact; the same quantity given two
different values; missing or hollow limitations in a document making empirical claims; the core finding absent from the abstract/summary.
- P1 (comprehension-breaking): jargon undefined for the stated audience; key
uncertainty unreported on small-N results; audience misfit in a load-bearing section; materially misleading structure.
- P2 (polish): tone, wordiness, formatting, staleness, minor flow issues.
Procedure: 1. Read the whole document once for the lede: what is the single most important claim? Check it appears in the abstract/summary (buried-lede). 2. Walk the [judge] criteria in checklist order. For each violation, record a finding. 3. Sweep all numbers: build a list of every quantitative claim; cross-check repeated quantities for consistency (inconsistent-number) and for uncertainty reporting (uncertainty-reported). 4. Sweep tone: hunt superlatives, buzzwords, and sales framing (marketing-tone). 5. Audience pass: simulate the stated reader; mark every place they would stall (audience-fit, jargon-undefined). 6. Self-check: delete any finding lacking a verbatim quote; downgrade any low-confidence high-severity finding; merge duplicates.
Output — JSON list only, no prose, each finding:
{
"check_id": "<id from checklist>",
"lane": "judge",
"severity": "P0|P1|P2",
"confidence": "high|medium|low",
"location": "<section heading or §N + brief locator>",
"evidence_quote": "<verbatim excerpt>",
"rationale": "<why this violates the rule — one or two sentences>",
"suggested_fix": "<concrete edit, not 'improve clarity'>"
}Also include, as the final array element, a summary pseudo-finding: {"check_id": "summary", "lane": "judge", "severity": "P2", "confidence": "high", "location": "document", "evidence_quote": "", "rationale": "<2-3 sentence overall assessment>", "suggested_fix": "<top 3 priorities in order>"}.
If the document is clean on a criterion, emit nothing for it. An empty list (plus summary) is a valid, good result — do not manufacture findings to seem thorough.
White-paper audit checklist (v1)
Research-grounded criteria (sources: Gopen & Swan; Nielsen Norman; Tufte; IEEE ProComm & AU plain-language guidance; white-paper practice guides — full list in internal/research_report_20260603_201802.md of the originating session, summarized at bottom). Each criterion is an operational rule with an example and counterexample, tagged [script] (checked by scripts/check_doc.py) or [judge] (LLM judge via references/audit-prompt.md), with default severity. Severity is always impact × confidence; the judge may move a finding ±1 level with stated rationale.
A. Structure & flow
| id | Rule | Lane | Default |
|---|---|---|---|
structure (abstract) | A labeled abstract / executive summary (~150–250 words) exists and states problem + key finding + recommendation. | script (presence) + judge (content) | P1 |
buried-lede | The single most important finding appears in the abstract/intro, not only in §5+. Bad: headline result first stated on page 9. OK: abstract states it, body elaborates. | judge | P0 if the core claim is absent up front; else P1 |
problem-first | The intro defines a concrete problem and its impact before methods/solutions. Bad: opens with architecture. OK: opens with who is harmed and how. | judge | P1 |
flow | Sections follow problem → background → approach → evidence → conclusions; transitions don't jump. | judge | P2 |
B. Clarity & readability
| id | Rule | Lane | Default |
|---|---|---|---|
readability | FK grade per section ≤ target (default 13; practitioner papers aim 10–12). Trend-level metric — see severity gating in DESIGN.md. | script | P2 (P1 in abstract/intro if > target+3) |
acronym-undefined | Every acronym is defined at/before first use (Full Term (ABC) or reverse) or in a glossary. Allowlist for universal ones (PDF, URL…). | script | P1 |
jargon-undefined | Field jargon (non-acronym terms of art) is glossed on first use for the stated audience. Bad: "our ablation shows…" with no gloss for practitioners. OK: "we measured every layer alone and in combination (the ablation)". Soft finding — the script can't judge audience fit. | judge | P1 (low/medium confidence) |
sentence-length | Average sentence ≲20–25 words; flag walls of dense text. | script (via readability) + judge | P2 |
scannability | Headings are informative; lists used for series; key terms emphasized. | judge | P2 |
C. Evidence & rigor
| id | Rule | Lane | Default |
|---|---|---|---|
unsupported-claim | Every factual/quantitative claim is supported by data in the doc, a citation, or an explicit hedge. The judge flags "apparent overclaim / unsupported claim" and "needs verification" — it does NOT adjudicate external truth (no retrieval in v1). Bad: "this guarantees GDPR compliance." OK: "benchmark success is not GDPR certification." | judge | P0 when stated as fact; P1 when hedged but thin |
inconsistent-number | The same quantity must match everywhere it appears (abstract vs body vs tables). Bad: abstract says 0.88, §5 says 0.78 for the same metric. | judge | P0 |
uncertainty-reported | Quantitative results carry uncertainty (CIs, "directional", N) where the underlying data is small/noisy. | judge | P1 |
limitations-present | A limitations section exists and is substantive (covers scope, sample size, biases — not a fig leaf). | script (presence) + judge (substance) | P0 (presence) / P1 (substance) |
marketing-tone | No hype: superlatives ("revolutionary", "unprecedented"), buzzwords, or sales-pitch framing in technical claims. Nielsen: removing "marketese" measurably improves comprehension. Bad: "our groundbreaking stack." OK: "the stack reaches 0.88 coverage recall (CI 0.85–0.90)." | judge | P0 if a claim rests on hype; else P2 |
consistent-units | Same units/terms throughout; no silent unit switches. | judge | P1 |
D. Tables & figures
| id | Rule | Lane | Default |
|---|---|---|---|
table-standalone | Tables/figures are numbered or clearly captioned, columns labeled with units, readable without hunting through text. | judge | P2 |
table-clutter | Minimal ruling/decoration (Tufte): data, not chartjunk. | judge | P2 |
E. Audience fit (practitioner focus)
| id | Rule | Lane | Default |
|---|---|---|---|
audience-fit | Explanations work for the stated audience (e.g. final-year school reader): concepts built up, examples concrete, no leaps that require unstated background. | judge | P1 |
concrete-examples | Abstract concepts are illustrated with tangible scenarios. Good: the quasi-identifier cascade with population fractions. | judge | P2 |
actionable-close | Conclusions give the reader specific next steps, not just "more research needed". | judge | P2 |
F. Trust & transparency
| id | Rule | Lane | Default |
|---|---|---|---|
structure (date/version, author) | Publication date or version + author/affiliation visible near the top. | script | P1 |
links | All links resolve (relative paths exist; http(s) reachable). Severity by materiality: claim-supporting link broken → P1; footer nicety → P2. | script + judge (materiality) | P1/P2 |
disclosure | Funding/sponsor/conflict disclosed if any; data/code availability stated for empirical claims. | judge | P1 |
currency | Citations and benchmarks not stale for a fast-moving field (~5 y rule of thumb). | judge | P2 |
Severity reference (impact × confidence)
- P0 — trust-breaking: apparent overclaim stated as fact · internally inconsistent
number · missing/hollow limitations in an empirical doc · core claim absent from abstract.
- P1 — comprehension-breaking: acronym/jargon undefined at first use · claim-supporting
broken link · missing date/author · uncertainty absent on small-N results · audience misfit.
- P2 — polish: tone nits · long sentences in non-critical sections · table formatting ·
dead footer links · staleness.
Low-confidence + high-impact ⇒ report at the lower severity with a "verify" note — never silently promote.
#!/usr/bin/env python3
"""Deterministic lane of the whitepaper-audit skill.
Stdlib-only checks over a markdown document:
readability Flesch-Kincaid grade (heuristic syllables; trend-level)
acronym-undefined acronyms used before any definition (allowlist-aware)
structure required blocks present (title, abstract, date, author,
limitations, glossary)
links relative links exist on disk; http(s) HEAD/GET unless --offline
Outputs a JSON list of findings (schema per DESIGN.md v0.2) on stdout, exit 0.
Never crashes on malformed markdown.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import urllib.request
from pathlib import Path
# ---------------------------------------------------------------- findings
def _finding(check_id, severity, confidence, location, evidence, rationale, fix):
return {
"check_id": check_id,
"lane": "script",
"severity": severity,
"confidence": confidence,
"location": location,
"evidence_quote": evidence,
"rationale": rationale,
"suggested_fix": fix,
}
# ---------------------------------------------------------------- markdown
CODE_BLOCK_RE = re.compile(r"```.*?(```|\Z)", re.S)
TABLE_ROW_RE = re.compile(r"^\s*\|.*$", re.M)
HEADING_RE = re.compile(r"^#{1,6} .*$", re.M)
LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)\s]*)\)")
URL_RE = re.compile(r"https?://\S+")
HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.S)
def strip_markdown(text: str) -> str:
"""Remove code blocks, tables, headings, URLs, comments; keep link text."""
text = CODE_BLOCK_RE.sub(" ", text)
text = HTML_COMMENT_RE.sub(" ", text)
text = HEADING_RE.sub(" ", text)
text = TABLE_ROW_RE.sub(" ", text)
text = LINK_RE.sub(r"\1", text)
text = URL_RE.sub(" ", text)
text = re.sub(r"[*_`>#]+", " ", text)
return re.sub(r"[ \t]+", " ", text)
# ---------------------------------------------------------------- readability
VOWELS = "aeiouy"
def _syllables(word: str) -> int:
word = re.sub(r"[^a-z]", "", word.lower())
if not word:
return 0
groups = len(re.findall(r"[aeiouy]+", word))
if word.endswith("e") and groups > 1 and not word.endswith(("le", "ee")):
groups -= 1
return max(1, groups)
def _sentences(text: str) -> list[str]:
parts = re.split(r"(?<=[.!?])\s+", text)
return [p.strip() for p in parts if len(p.split()) >= 3]
def fk_grade(text: str) -> float:
"""Flesch-Kincaid grade level (heuristic, trend-level only)."""
sents = _sentences(text) or [text]
words = [w for s in sents for w in re.findall(r"[A-Za-z']+", s)]
if not words:
return 0.0
syl = sum(_syllables(w) for w in words)
return 0.39 * (len(words) / len(sents)) + 11.8 * (syl / len(words)) - 15.59
def hardest_sentences(text: str, n: int = 5) -> list[str]:
sents = _sentences(strip_markdown(text))
return sorted(sents, key=fk_grade, reverse=True)[:n]
def _readability_findings(text: str, target: float) -> list[dict]:
findings = []
sections = re.split(r"^(#{1,3} .*)$", text, flags=re.M)
# pair headings with bodies; leading chunk = preamble
pairs = [("(preamble)", sections[0])]
for i in range(1, len(sections) - 1, 2):
pairs.append((sections[i].lstrip("# ").strip(), sections[i + 1]))
critical = re.compile(r"abstract|summary|one paragraph|introduction|overview", re.I)
for name, body in pairs:
prose = strip_markdown(body)
if len(prose.split()) < 30:
continue
grade = fk_grade(prose)
if grade <= target:
continue
is_critical = bool(critical.search(name))
sev = "P1" if (is_critical and grade > target + 3) else "P2"
top = hardest_sentences(body, 3)
findings.append(_finding(
"readability", sev, "medium", f"section: {name}",
(top[0][:200] if top else prose[:200]),
f"FK grade ≈ {grade:.1f} exceeds target {target:g} "
f"(heuristic, trend-level).",
"Shorten sentences; prefer concrete words. Hardest sentences: "
+ " | ".join(t[:120] for t in top),
))
return findings
# ---------------------------------------------------------------- acronyms
ALLOWLIST = {
"PDF", "URL", "URLS", "USA", "EU", "US", "UK", "IT", "AI", "API", "ID",
"IDS", "FAQ", "ISO", "CEO", "CTO", "OK", "TV", "GB", "MB", "KB", "CPU",
"GPU", "RAM", "HTML", "HTTP", "HTTPS", "JSON", "CSV", "YAML", "TODO",
"README", "MIT", "CC", "BY",
}
ACRONYM_RE = re.compile(r"\b[A-Z][A-Z0-9]{1,5}\b")
def _acronym_findings(text: str, allowlist=frozenset()) -> list[dict]:
body = CODE_BLOCK_RE.sub(" ", text)
body = URL_RE.sub(" ", body)
allow = ALLOWLIST | {a.upper() for a in allowlist}
glossary = set()
gloss_m = re.search(r"^#{1,3} .*glossar.*$", body, re.I | re.M)
if gloss_m:
for row in TABLE_ROW_RE.findall(body[gloss_m.end():]):
glossary.update(a.upper() for a in ACRONYM_RE.findall(row))
findings, seen = [], set()
for m in ACRONYM_RE.finditer(body):
acro = m.group(0)
base = acro[:-1] if acro.endswith("S") and len(acro) > 2 else acro
if base in seen or acro in seen:
continue
seen.add(base)
if base in allow or acro in allow or base in glossary:
continue
before = body[:m.end() + 200] # definition may trail the first use
defined = (
re.search(re.escape(base) + r"s?\s*\(", before) and
re.search(re.escape(base) + r"s?\s*\([A-Z]", before)
) or re.search(r"\(\s*" + re.escape(base) + r"s?\s*\)", before)
if defined:
continue
line = body[:m.start()].count("\n") + 1
findings.append(_finding(
"acronym-undefined", "P1", "high", f"line {line}",
body[max(0, m.start() - 40):m.end() + 40].strip(),
f"Acronym '{base}' is used without a definition at or before "
"first use, and is not in the glossary.",
f"Define on first use: 'Full Term ({base})' — or add a glossary "
"entry.",
))
return findings
# ---------------------------------------------------------------- structure
BLOCKS = {
"title": (r"^# .+", "P1", "high"),
"abstract/summary": (r"^#{1,3} .*(abstract|summary|one paragraph|overview|tl;dr)", "P1", "medium"),
"date/version": (r"version\s*\d|\b(19|20)\d{2}\b", "P1", "medium"),
"author": (r"author|contributors|by\s+[A-Z][a-z]+ [A-Z]", "P1", "low"),
"limitations": (r"^#{1,3} .*(limitation|caveat|known issue)", "P0", "medium"),
"glossary": (r"^#{1,3} .*(glossar|terminolog|definitions)", "P2", "medium"),
}
def _structure_findings(text: str) -> list[dict]:
head = "\n".join(text.splitlines()[:30])
findings = []
for name, (pat, sev, conf) in BLOCKS.items():
scope = head if name in {"title", "date/version", "author"} else text
if re.search(pat, scope, re.I | re.M):
continue
findings.append(_finding(
"structure", sev, conf, "document", f"missing block: {name}",
f"No {name} block detected. "
+ ("Empirical claims without a limitations section are a trust "
"risk." if name == "limitations" else
"Expected in a practitioner-facing white paper."),
f"Add a {name} section."
+ (" If terms are defined inline throughout, a glossary may be "
"optional." if name == "glossary" else ""),
))
return findings
# ---------------------------------------------------------------- links
def _check_url(url: str, timeout: float = 5.0) -> str:
for method in ("HEAD", "GET"):
try:
req = urllib.request.Request(url, method=method,
headers={"User-Agent": "whitepaper-audit/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
if resp.status < 400:
return "ok"
if resp.status in (404, 410):
return "broken"
except urllib.error.HTTPError as e:
if e.code in (404, 410):
return "broken"
return "uncertain"
except Exception:
continue
return "unreachable"
def _link_findings(text: str, base_dir, offline: bool) -> list[dict]:
findings = []
body = CODE_BLOCK_RE.sub(" ", text)
for m in LINK_RE.finditer(body):
label, target = m.groups()
if not target or target.startswith(("#", "mailto:")):
continue
if target.startswith(("http://", "https://")):
if offline:
continue
if _check_url(target) != "broken":
continue
status = "broken (HTTP 404/410)"
else:
if base_dir is None:
continue
if (Path(base_dir) / target.split("#")[0]).exists():
continue
status = "missing on disk"
line = body[:m.start()].count("\n") + 1
findings.append(_finding(
"links", "P1", "high", f"line {line}",
f"[{label}]({target})",
f"Link target is {status}.",
"Fix the path/URL or remove the link.",
))
return findings
# ---------------------------------------------------------------- entry
def check_document(text: str, *, base_dir=None, offline: bool = True,
target_grade: float = 13.0, allowlist=()) -> list[dict]:
try:
return (
_readability_findings(text, target_grade)
+ _acronym_findings(text, allowlist)
+ _structure_findings(text)
+ _link_findings(text, base_dir, offline)
)
except Exception as e: # never crash on malformed input
return [_finding("internal", "P2", "low", "document", str(e)[:200],
"check_doc internal error on this input.",
"Report this input to the skill maintainer.")]
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("file", type=Path)
ap.add_argument("--offline", action="store_true")
ap.add_argument("--target-grade", type=float, default=13.0)
ap.add_argument("--allow", action="append", default=[],
help="extra allowlisted acronyms")
args = ap.parse_args(argv)
text = args.file.read_text(encoding="utf-8", errors="replace")
findings = check_document(
text, base_dir=args.file.parent, offline=args.offline,
target_grade=args.target_grade, allowlist=args.allow)
json.dump(findings, sys.stdout, indent=2, ensure_ascii=False)
print()
return 0
if __name__ == "__main__":
sys.exit(main())
"""TDD suite for check_doc.py — deterministic lane of the whitepaper-audit skill.
Written BEFORE the implementation (RED phase). Behaviors per DESIGN.md v0.2.
"""
import json
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from check_doc import ( # noqa: E402
check_document,
fk_grade,
hardest_sentences,
strip_markdown,
)
# ---------------------------------------------------------------- helpers
FULL_DOC = """# Great Paper
*Version 1.0 · 2026-06-03 · Jane Author*
## Abstract
We measure things carefully. The cat sat on the mat. NER (Named Entity Recognition)
tags names in text. Later we use NER again freely.
## Method
We did the work.
## Limitations
Small sample. Results are directional.
## Glossary
| Term | Meaning |
|---|---|
| **GDPR** | EU privacy law. |
"""
def findings_by_check(text, **kw):
out = {}
for f in check_document(text, **kw):
out.setdefault(f["check_id"], []).append(f)
return out
# ---------------------------------------------------------------- schema
def test_findings_conform_to_schema():
doc = "# T\n\nUNDEFINEDACRO is used here without explanation.\n"
for f in check_document(doc):
assert f["lane"] == "script"
assert f["severity"] in {"P0", "P1", "P2"}
assert f["confidence"] in {"high", "medium", "low"}
for key in ("check_id", "location", "evidence_quote", "rationale",
"suggested_fix"):
assert key in f, f"missing {key}"
def test_malformed_markdown_does_not_crash():
junk = "# x\n``` unclosed\n| a | b\n[link(](broke\n\x00weird"
assert isinstance(check_document(junk), list)
# ---------------------------------------------------------------- readability
def test_fk_grade_simple_lower_than_complex():
simple = "The cat sat. The dog ran. We like it. It is good."
complex_ = ("Notwithstanding considerable epistemological heterogeneity, "
"operationalization of multidimensional anonymization "
"methodologies necessitates comprehensive interdisciplinary "
"collaboration across institutional infrastructures.")
assert fk_grade(simple) < fk_grade(complex_)
def test_strip_markdown_removes_code_tables_headings_urls():
md = ("## Heading Words\n\n"
"Real prose stays.\n\n"
"```python\ncode_tokens_disappear()\n```\n\n"
"| col | gone |\n|---|---|\n| x | y |\n\n"
"A [link text](https://example.com/very-long-url) stays as text.\n")
out = strip_markdown(md)
assert "Real prose stays" in out
assert "link text" in out
assert "code_tokens_disappear" not in out
assert "https://example.com" not in out
assert "Heading Words" not in out
assert "| col |" not in out
def test_hardest_sentences_returns_at_most_n_sorted_hard_first():
text = ("The cat sat on the mat. "
"Operationalization of multidimensional anonymization "
"methodologies necessitates comprehensive interdisciplinary "
"collaboration. "
"Dogs run fast.")
top = hardest_sentences(text, n=2)
assert len(top) == 2
assert "Operationalization" in top[0]
def test_readability_finding_emitted_when_over_target():
hard = ("# T\n\n## Abstract\n\n" +
("Operationalization of multidimensional anonymization "
"methodologies necessitates comprehensive interdisciplinary "
"collaboration across heterogeneous institutional "
"infrastructures. ") * 3)
by = findings_by_check(hard, target_grade=8)
assert "readability" in by
def test_no_readability_finding_for_simple_doc_with_high_target():
by = findings_by_check(FULL_DOC, target_grade=30)
assert "readability" not in by
# ---------------------------------------------------------------- acronyms
def test_acronym_defined_inline_not_flagged():
doc = "# T\n\nNER (Named Entity Recognition) tags text. NER is useful.\n"
assert "acronym-undefined" not in findings_by_check(doc)
def test_acronym_reverse_definition_not_flagged():
doc = "# T\n\nNamed Entity Recognition (NER) tags text. NER is useful.\n"
assert "acronym-undefined" not in findings_by_check(doc)
def test_undefined_acronym_flagged_P1_high():
doc = "# T\n\nThe CQRS pattern is great. CQRS everywhere.\n"
by = findings_by_check(doc)
assert "acronym-undefined" in by
f = by["acronym-undefined"][0]
assert f["severity"] == "P1"
assert f["confidence"] == "high"
assert "CQRS" in f["evidence_quote"]
def test_allowlisted_acronyms_not_flagged():
doc = "# T\n\nExport a PDF from the URL in the USA.\n"
assert "acronym-undefined" not in findings_by_check(doc)
def test_glossary_entry_counts_as_definition():
doc = ("# T\n\nGDPR applies here.\n\n## Glossary\n\n"
"| Term | Meaning |\n|---|---|\n| **GDPR** | EU privacy law. |\n")
assert "acronym-undefined" not in findings_by_check(doc)
def test_plural_acronym_use_after_definition_not_flagged():
doc = "# T\n\nLLM (Large Language Model) tools. LLMs are everywhere.\n"
assert "acronym-undefined" not in findings_by_check(doc)
# ---------------------------------------------------------------- structure
def test_complete_doc_has_no_structure_findings():
assert "structure" not in findings_by_check(FULL_DOC)
def test_missing_limitations_is_P0_candidate():
doc = FULL_DOC.replace("## Limitations\n\nSmall sample. Results are directional.\n\n", "")
by = findings_by_check(doc)
assert "structure" in by
lim = [f for f in by["structure"] if "limitation" in f["rationale"].lower()
or "limitation" in f["evidence_quote"].lower()]
assert lim and lim[0]["severity"] == "P0" and lim[0]["confidence"] == "medium"
def test_missing_glossary_is_P2():
doc = FULL_DOC.split("## Glossary")[0]
by = findings_by_check(doc)
gl = [f for f in by.get("structure", []) if "glossary" in
(f["rationale"] + f["evidence_quote"]).lower()]
assert gl and gl[0]["severity"] == "P2"
# ---------------------------------------------------------------- links
def test_relative_link_to_missing_file_flagged(tmp_path):
doc = "# T\n\nSee [the data](data/gone.csv).\n"
by = findings_by_check(doc, base_dir=tmp_path)
assert "links" in by
assert "data/gone.csv" in by["links"][0]["evidence_quote"]
def test_relative_link_to_existing_file_ok(tmp_path):
(tmp_path / "real.md").write_text("hi")
doc = "# T\n\nSee [real](real.md).\n"
assert "links" not in findings_by_check(doc, base_dir=tmp_path)
def test_http_links_skipped_when_offline():
doc = "# T\n\nSee [site](https://definitely-not-a-real-host-xyz.example).\n"
assert "links" not in findings_by_check(doc, offline=True)
# ---------------------------------------------------------------- CLI
def test_cli_outputs_json_and_exits_zero(tmp_path):
p = tmp_path / "doc.md"
p.write_text("# T\n\nCQRS everywhere.\n")
script = Path(__file__).resolve().parents[1] / "check_doc.py"
r = subprocess.run([sys.executable, str(script), str(p), "--offline"],
capture_output=True, text=True)
assert r.returncode == 0
data = json.loads(r.stdout)
assert isinstance(data, list)
assert any(f["check_id"] == "acronym-undefined" for f in data)