diff --git a/docs/explanations/2026-07-17-explanation-byox-ladder.html b/docs/explanations/2026-07-17-explanation-byox-ladder.html new file mode 100644 index 00000000..71d80df4 --- /dev/null +++ b/docs/explanations/2026-07-17-explanation-byox-ladder.html @@ -0,0 +1,366 @@ + + + + + +Explaining: the byox_ladder difficulty generator + + + +
+

Building a difficulty ladder generator

+

How python_pkg/byox_ladder turns the build-your-own-x README into two ranked, offline HTML pages.

+

commit 6342eaf · testsAndMisc · 2026-07-17

+ + + +

1 · Background

+

build-your-own-x + is a hugely popular README: a curated list of ~360 step-by-step tutorials for re-creating technologies + from scratch — a 3D renderer, a Git, a database, an operating system. The README groups guides under + 30 category headings like #### Build your own `Regex Engine`, and each guide is one bullet:

+
* [**Go**: _Building a BitTorrent client from the ground up_](https://blog.jse.li/posts/torrent/)
+

The task was to sort all of this from easiest to hardest. This commit adds a small, + self-contained tool that does exactly that and emits two browsable pages: a category ladder + (the 30 headings) and a guide ladder (every individual tutorial), each grouped into four + difficulty tiers — Beginner, Intermediate, Advanced, Expert — plus an Unsorted bucket.

+

It lives in testsAndMisc, a monorepo with strict gates: all Python must sit under + python_pkg/, hit 100% branch coverage, and pass ruff (select=ALL), + mypy, pylint 10/10, and bandit. Those gates shaped the design as much as the feature did — more on that in + the Code section.

+ +

2 · Intuition

+

The heart of the change is one question: how do you assign a difficulty to a single guide? + The naïve answer — give every guide a unique rank from 1 to 360 — is a trap. There's no honest signal that + says tutorial #147 is harder than #148. So the design deliberately refuses that false precision and uses a + two-part rule instead:

+ +
+
1 · CategoryThe guide's category sets its tier and a base rank + (1–30). This is the dominant signal.
+
→
+
2 · LanguageThe language adds a tiny nudge (0.2–0.9). Lower-level + languages nudge higher.
+
→
+
3 · Scorerank×10 + nudge. Sorting by score keeps whole tiers + contiguous.
+
+ +

Because the nudge is always under 1 and the tier gap is 10 per rank, language can reorder guides + inside a tier but can never move one across a tier line. An operating-system guide is Expert whether + it's written in C or Python; the language just decides which OS guide you might reach for first.

+ + +

Score bands. Rank 1–5 → Beginner, 6–12 → Intermediate, 13–22 → Advanced, 23–30 → Expert.

+ +

Play with the rule

+

Pick a category and a language. Watch the score, the tier, and where this guide lands among the other + languages in the same category. Notice the tier badge only changes when the category changes.

+ +
+
+ + +
+
score = …
+
Tier: —
+ +

+

Within-category order (by score, easiest first)

+
    +
    +

    This widget runs the real scoring logic from parse_guides.py — + the same CATEGORIES and LANG_EFFORT tables, the same + min()-of-languages nudge, the same rank×10 + nudge score.

    + +

    3 · Code

    + +

    a · The scoring transform — parse_guides.py

    +

    The parser walks the README line by line, tracking the current category header, and turns each guide + bullet into a record. Two small pure functions carry the difficulty model:

    +
    def lang_effort(langs):
    +    # the EASIEST listed language wins — you'd pick your best one
    +    values = [LANG_EFFORT.get(l.lower(), DEFAULT_EFFORT) for l in langs]
    +    return min(values) if values else DEFAULT_EFFORT
    +
    +# inside parse_guide_line(...):
    +rank, tier = CATEGORIES.get(category, (UNSORTED_RANK, "unsorted"))
    +score = round(rank * 10 + lang_effort(langs), 2)
    +

    A guide with an unknown category falls through to UNSORTED_RANK = 99 and the + "unsorted" tier — that's how the repo's own "Uncategorized" bucket is kept honest instead of + being force-fit into a difficulty tier.

    + +

    b · From data to a runnable page — build_pages.py

    +

    The guide page is a template with a placeholder. The builder inlines the parsed data as a JS constant and + wraps everything in a minimal HTML skeleton so the file opens straight from disk:

    +
    payload = json.dumps(guides, separators=(",", ":")).replace("</", "<\\/")
    +content = template.replace(MARKER, f"const GUIDES = {payload};")
    +return standalone(content)   # adds <!doctype html>…</html>
    +

    The replace("</", "<\\/") guard matters: the data is dropped inside a + <script> tag, and any literal </script> in a tutorial title would + otherwise close the block early. Everything is inlined — no fonts, no CDN, no network — so the page works + offline and in your OS light/dark theme.

    + +

    c · Reproducible, not hand-built

    +

    A Makefile chains the pipeline; the fetched README and guides.json are + gitignored derived artifacts, while the built *-ladder.html pages are committed as the runnable + output. Re-running make build reproduces byte-identical HTML — the sign of a deterministic build.

    +
    build: guides.json
    +	python3 build_pages.py
    +guides.json: byox-readme.md parse_guides.py
    +	python3 parse_guides.py
    +byox-readme.md:
    +	curl -fsSL "$(README_URL)" -o byox-readme.md
    + +

    d · Tests to 100% branch coverage

    +

    Because the code lives under python_pkg/, the repo's fail_under=100 gate applies. + The two functions with branches — lang_effort (empty vs non-empty) and the parse/build error + paths — are each exercised from both sides. lang_effort([]) is called directly to cover the + else branch that normal input can't reach.

    + +

    e · Governance artifacts

    +

    The monorepo rejects any code commit that lacks a validated evidence JSON (intent, scope, + verification with real command output, rollback), and a contract JSON once a change spans + four or more code files. One line was also added to .pre-commit-config.yaml to exclude the + generated guide page from codespell, since it inlines upstream titles verbatim — including a genuine + spelling slip carried over from one of the upstream tutorial titles.

    + +

    4 · Quiz

    +

    Five questions on the reasoning behind the change. Click to reveal.

    +
    +
    Score: 0 / 0
    + +
    Generated by /explain-diff · adapts Geoffrey Litt's "understanding is the + bottleneck" idea. Diagrams and the widget run the change's real logic.
    +
    + + + + diff --git a/docs/explanations/index.html b/docs/explanations/index.html index 9ebfb460..fa9a39e0 100644 --- a/docs/explanations/index.html +++ b/docs/explanations/index.html @@ -30,12 +30,16 @@
    explanations

    Change explanations

    -

    7 explanations · interactive walkthroughs of notable changes, newest first.

    +

    8 explanations · interactive walkthroughs of notable changes, newest first.

    2026-07-17 Night lockdown: keeping the servers up through the curfew + + 2026-07-17 + Explaining: the byox_ladder difficulty generator + 2026-07-17 Reporting the truth: brother_printer status accuracy