Skip to main content

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.

CI Python License: MIT Status: Alpha

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-severity exits 2 when any step scores past your threshold; a ready-made GitHub Actions workflow is in examples/
  • 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

  1. Fetch. URLs are requested with an Accept header 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.
  2. 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.
  3. 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.
  4. 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_actionable comes back likely.
  5. Interpret. Probabilities become findings through Thresholds in evaluate.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.
  6. 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-run shows 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)

Source distribution for docfriction 0.1.0
File Size Uploaded
docfriction-0.1.0.tar.gz 100.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for docfriction 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page