docfriction
Friction logs for documentation pages, generated by walking a page section by section and asking TypeSafe Jev a fixed set of typed questions about each one.
A friction log is the write-up a developer advocate produces after going through a quickstart as a new user: one entry per step, what they expected, where they got stuck, how bad it was. Teams get a lot out of them and rarely have time to write them, so most pages never get one and the ones that exist go stale on the next edit.
docfriction produces the same document from a URL or a Markdown file:
- One step per heading — the page is split at headings; each step carries its heading path, prose, code blocks and links, plus a summary of the previous section as context
- One Jev call per step — friction type (a choice over eight categories plus
no_friction), severity (a score over four reader outcomes), and yes/no checks for prerequisites, expected result, undefined terms, code/prose match and unexplained placeholders, batched into a single request - Deterministic checks for what Jev can't do — dead links (
--check-links), code blocks with no language tag, stub sections, placeholder detection - Calibrated output — every finding carries Jev's probability and confidence; answers in the uncertain band are labelled "needs review" instead of being reported as findings
- CI gate —
--fail-on-severityexits 2 when any step scores past your threshold; a ready-made GitHub Actions workflow is inexamples/ - Cheap enough to run on every change — Jev charges $0.042 per million input tokens and answers in roughly 100 ms, so a 40-section page costs well under a cent and finishes in a few seconds
$ docfriction https://docs.example.com/quickstart --check-links
# Friction log: Quickstart
Steps: 9 · steps with friction: 2 · max severity: 2.4/3 · Jev tokens: 4,120 in, 210 out (about $0.0002; output is free)
| # | Step | Sentiment | Severity | Friction |
|---|------------------------------|---------------|----------|-------------------------------------------|
| 1 | Quickstart | 😀 smooth | 0.2 | – |
| 2 | Quickstart > Install | 😀 smooth | 0.3 | – |
| 3 | Quickstart > Configure | 🛑 blocked | 2.4 | missing_prerequisite (0.81), dead_link |
| 4 | Quickstart > First request | 😐 pause | 1.1 | – |
...
A full report is in examples/sample-friction-log.md. The notes on Jev that shaped the design are in docs/research.md.
Installation
Not on PyPI yet. Install from GitHub:
uv tool install git+https://github.com/Cyvid7-Darus10/docfriction # or: pipx install git+https://github.com/Cyvid7-Darus10/docfriction
export TYPESAFE_API_KEY=... # from https://typesafe.ai
Requires Python 3.10+. --dry-run works without a key. To work on it:
git clone https://github.com/Cyvid7-Darus10/docfriction
cd docfriction && uv sync
uv run docfriction --help
Usage
# A live docs page, Markdown report to stdout
docfriction https://docs.example.com/quickstart
# A local file, JSON report to disk, and check every external link
docfriction docs/quickstart.md --format json --out friction.json --check-links
# See how the page splits into steps before spending any tokens
docfriction https://docs.example.com/quickstart --dry-run
# CI: exit 2 if any step scores 2.5 or higher on the 0-3 severity scale
docfriction docs/quickstart.md --fail-on-severity 2.5
| Flag | What it does |
|---|---|
-f, --format md|json |
Report format (default md) |
-o, --out FILE |
Write the report to a file instead of stdout |
--check-links |
HEAD every external link in each section and report 4xx/5xx. Links that resolve to private, loopback or link-local addresses are reported as blocked_link and never requested |
--allow-private-links |
Let --check-links request private addresses, e.g. a docs server on localhost |
--max-sections N |
Only evaluate the first N sections |
--concurrency N |
Parallel Jev calls (default 4) |
--model ID |
Pin a Jev version, e.g. jev-1.13.0 (default jev-latest) |
--fail-on-severity N |
Exit 2 when any step's severity is at least N |
--dry-run |
Print the detected steps and exit without calling Jev |
Exit codes: 0 ok, 1 fetch or API error, 2 severity threshold exceeded.
Why Jev
Jev doesn't generate text. You send it a state and a set of typed questions (yes/no, pick one, rate on a scale) and it returns one typed answer per question with a probability distribution and a confidence number. The judgments in a friction log are all of that shape:
| Question a human answers per step | Jev primitive docfriction uses |
|---|---|
| What went wrong here? | Choice over eight friction types plus no_friction |
| How bad was it? | Score over four situations, from "continued without noticing" to "cannot complete the step" |
| Did the page tell me what I needed first? | Noul (yes/no) prerequisites_stated |
| Do I know whether it worked? | Noul expected_result_shown |
| Does the code do what the text says? | Noul code_matches_prose |
Where do I get YOUR_API_KEY? |
Noul placeholders_explained, only asked when a regex finds placeholders |
Because the answers are numbers, thresholds decide what counts as a finding and CI can fail on severity. Jev can't count, compare versions, follow links or explain itself, so those parts are ordinary code in checks.py.
How it works
- Fetch. URLs are requested with an
Acceptheader that prefers Markdown, which many docs sites serve. HTML is reduced to its<article>/<main>content and converted to Markdown with fenced, language-tagged code blocks. - Segment. The Markdown is split at ATX (
#) and setext (underlined) headings. Each segment keeps its heading path, prose, fenced and indented code blocks, and links. Front matter and HTML comments are dropped. Prose over 6,000 characters is truncated so the Jev state stays small; Jev degrades on large irrelevant context. - Build state. Each step is evaluated the way a reader meets it: a summary of the previous section, the section text, the code blocks, and any placeholders the regex found.
- Ask Jev once per step. Every rubric question goes in one request. Questions that only apply to actionable steps are still asked, and only applied when
is_actionablecomes back likely. - Interpret. Probabilities become findings through
Thresholdsinevaluate.py. A yes/no check below 0.4 is a finding; between 0.4 and 0.6 it is "needs review". A friction type is reported when its probability is at least 0.5 and marked for review when Jev's confidence is under 0.4. - Report. Markdown laid out like a hand-written friction log (summary table, then a walkthrough with a sentiment per step), or JSON for tooling.
Sentiment maps the 0-3 severity score to the marker a human would put in the margin: 😀 smooth (under 0.75), 😐 pause (under 1.5), 😠 frustrated (under 2.25), 🛑 blocked.
Library use
from docfriction import EvaluateOptions, JevClient, evaluate_document, load_document, render_markdown
document = load_document("https://docs.example.com/quickstart")
with JevClient() as client:
log = evaluate_document(document, client, EvaluateOptions(check_links=True))
print(render_markdown(log))
for step in log.friction_steps:
print(step.segment.title, step.severity, [f.check for f in step.confirmed_findings])
Tuning the rubric
Everything Jev is asked lives in rubric.py. The wording follows TypeSafe's guidance: judgments in instructions, answers in criteria, score levels that describe situations rather than degrees, a no_friction option so the choice is never forced, and backticked references to state fields. If your docs have a house rule (say, every step must end with expected output), add a Noul for it and a line in NOUL_FAILURE_DETAIL.
The default thresholds are starting points. Run docfriction on a few pages you know well, compare its findings with your own, and adjust Thresholds before wiring it into CI.
Status
- Not yet validated against human friction logs. Precision and recall per rubric item are unknown.
- Jev returns probabilities, not reasons. The report says which rubric item fired and how likely; a writer still has to read the section to decide what to change.
- Jev reads literally and isn't hardened against prompt injection, so a page can influence its own score. This is a quality tool, not a security control.
- One heading is one step. A tutorial under a single heading gets one coarse evaluation; a page of tiny headings gets many stub findings.
--dry-runshows the split before you spend tokens.
Development
uv sync --all-groups
uv run pytest --cov=docfriction # coverage gate is 80% in CI
uv run ruff check src tests && uv run ruff format src tests
uv run mypy src # strict
The Jev client is a thin httpx wrapper over the documented POST /v1/systemone endpoint with backoff on 429/529. Answers are validated against the questions that were asked, so a choice Jev was never offered can't reach a report. All tests run against httpx.MockTransport; no network or key is needed.
Contributing
Bug reports, rubric ideas and pull requests are welcome. CONTRIBUTING.md covers setup, the checks CI runs, and the rules for changing what we ask Jev. Security issues go through SECURITY.md. Releases are listed in CHANGELOG.md.
License
MIT. Authored by Cyrus David Pastelero.
Release files for docfriction 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| docfriction-0.1.0.tar.gz | 100.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| docfriction-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 126.4 kB
Release files / docfriction-0.1.0.tar.gz
| Download URL | docfriction-0.1.0.tar.gz |
|---|---|
| Size | 100.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e7b39755f3187ab603f24d38cc7b331394bbed29e926cb05ba09e273419fe38b
|
|
BLAKE2b-256 checksum How to use checksums |
033be00a2358a070a14bb1cedbb9adce2781ed97e00b5f0cc42cd23b25999820
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / docfriction-0.1.0-py3-none-any.whl
| Download URL | docfriction-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4c7d220256b27029c2ee4d445b545332a0ede0e9b283af5c64049d652f5b9d81
|
|
BLAKE2b-256 checksum How to use checksums |
28fef9b072a6f3b17b86ac486bc0bda491ad1c5696821283ad3550862539116a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log