diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24268c68..d3c39142 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -293,9 +293,7 @@ repos: args: - --skip=*.json,*.lock,*-lock.yaml,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif - # guide-ladder.html inlines upstream build-your-own-x tutorial titles - # verbatim (third-party data, incl. real source typos) — not authored text. - exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$|python_pkg/byox_ladder/guide-ladder\.html$) + exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$) # =========================================================================== # DOCFORMATTER - Format docstrings (disabled - causes recursion errors) diff --git a/CLAUDE.md b/CLAUDE.md index 4e2b4bd5..0522919d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,10 @@ Extracted to their own repos: `linux_configuration` dufs scripts + the standalone `dufs_client` repo. The live `media-cloud-sync` systemd units now run the scripts from `~/dufs-cloud/scripts/`. +- [`build-your-x`](https://github.com/kuhyx/build-your-x) — build-your-own-x + difficulty ladder + `byox` progress tracker (crdt_sync-backed) and the builds + themselves (`builds/`). Extracted from `python_pkg/byox_ladder/` + the + standalone `~/build_your_x` builds. Lives at `~/build_your_x`. Archived / unmaintained projects live in the sibling repository [`testsAndMisc-archive`](https://github.com/kuhyx/testsAndMisc-archive). diff --git a/docs/superpowers/evidence/remove-build-your-x-2026-07-17.json b/docs/superpowers/evidence/remove-build-your-x-2026-07-17.json new file mode 100644 index 00000000..4ccd60fc --- /dev/null +++ b/docs/superpowers/evidence/remove-build-your-x-2026-07-17.json @@ -0,0 +1,34 @@ +{ + "intent": "Remove byox_ladder from the monorepo after extracting it to its own GitHub repo (kuhyx/build-your-x), where it gained a byox progress tracker + crdt_sync sync alongside the builds.", + "scope": [ + "Delete python_pkg/byox_ladder/ (moved to https://github.com/kuhyx/build-your-x with full git history)", + "Update CLAUDE.md 'Extracted to their own repos' list", + "Drop the now-stale byox_ladder codespell exclude in .pre-commit-config.yaml", + "Non-goal: no behaviour change to any remaining monorepo package" + ], + "changes": [ + "git rm -r python_pkg/byox_ladder (generator + tests + built pages)", + "CLAUDE.md: add build-your-x to the extracted-repos list", + ".pre-commit-config.yaml: remove the guide-ladder.html codespell exclude + its stale comment" + ], + "verification": [ + { + "command": "grep -rn byox_ladder --include=*.py --include=*.toml --include=*.yaml . (excluding the package)", + "result": "pass", + "evidence": "No remaining references to byox_ladder anywhere in the monorepo after removal." + }, + { + "command": "new repo: python -m pytest byox_ladder/tests/ --cov=byox_ladder --cov-branch --cov-fail-under=100", + "result": "pass", + "evidence": "71 passed, 100.00% branch coverage in kuhyx/build-your-x (pre-push gate)." + } + ], + "risks": [ + "byox_ladder had no cross-package imports, so no remaining package depends on it.", + "Derived, gitignored artifacts (guides.json, byox-readme.md) were regenerable and copied into the new repo's checkout." + ], + "rollback": [ + "Clone https://github.com/kuhyx/build-your-x and restore byox_ladder/ under python_pkg/ with imports rewritten back to python_pkg.byox_ladder.*", + "Validate: pytest python_pkg/byox_ladder/tests/ passes at 100%." + ] +} diff --git a/python_pkg/byox_ladder/.gitignore b/python_pkg/byox_ladder/.gitignore deleted file mode 100644 index cdad6bdd..00000000 --- a/python_pkg/byox_ladder/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Fetched upstream README (third-party content — not vendored) and the derived -# dataset. The built *-ladder.html pages ARE committed as the runnable output. -byox-readme.md -guides.json diff --git a/python_pkg/byox_ladder/Makefile b/python_pkg/byox_ladder/Makefile deleted file mode 100644 index bee3e018..00000000 --- a/python_pkg/byox_ladder/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Regenerate the Build-Your-Own-X difficulty ladders. -# -# make build # fetch README -> parse -> build both standalone pages -# make clean # remove the fetched README copy and derived data - -README_URL = https://raw.githubusercontent.com/codecrafters-io/build-your-own-x/master/README.md - -.PHONY: build clean - -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 - -clean: - rm -f byox-readme.md guides.json diff --git a/python_pkg/byox_ladder/README.md b/python_pkg/byox_ladder/README.md deleted file mode 100644 index 07bb7168..00000000 --- a/python_pkg/byox_ladder/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Build-Your-Own-X Difficulty Ladder - -Two self-contained, offline HTML pages that rank the projects from -[codecrafters-io/build-your-own-x](https://github.com/codecrafters-io/build-your-own-x) -from easiest to hardest: - -- **`guide-ladder.html`** — every individual tutorial (~360 guides), grouped - into difficulty tiers and filterable by tier, language, and category. -- **`category-ladder.html`** — the 30 top-level categories, grouped into - Beginner / Intermediate / Advanced / Expert tiers. - -Both are single files with all CSS, JS, and data inlined — no server, fonts, or -network needed. Open either directly in a browser (`file://`) and it works -offline, in your OS light/dark theme. - -## Difficulty model - -Difficulty is a judgment call, scored on three axes: prerequisite domain -knowledge, minimal-version scope, and how much step-by-step guidance exists. - -- A guide **inherits its category's tier** — the dominant signal. A Regex-Engine - guide is easier than an Operating-System guide whatever the language. -- **Language only nudges the order _within_ a tier**: lower-level languages - (C, Rust, Assembly) sort a little harder than Python or JavaScript. It never - moves a guide across a tier boundary. -- The **Unsorted** group is the repo's own "Uncategorized" bucket. Its guides - span every level, so they are deliberately left un-ranked rather than forced - into a tier. - -The exact head-to-head order inside a tier is therefore approximate by design; -the tier boundaries are the trustworthy part. - -## Regenerating - -```sh -make build # curl the README -> parse_guides.py -> build_pages.py -make clean # remove the fetched README copy and guides.json -``` - -The pipeline: - -1. `parse_guides.py` — parses a local copy of the upstream README - (`byox-readme.md`) into `guides.json`, applying the difficulty model above. -2. `build_pages.py` — injects `guides.json` into `templates/guide.template.html` - and wraps `templates/category.html`, writing the two standalone pages. - -`byox-readme.md` and `guides.json` are derived artifacts and are gitignored; the -built `*-ladder.html` pages are committed as the runnable output. diff --git a/python_pkg/byox_ladder/__init__.py b/python_pkg/byox_ladder/__init__.py deleted file mode 100644 index 76eb5fcc..00000000 --- a/python_pkg/byox_ladder/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Generate the Build-Your-Own-X difficulty ladder pages. - -See ``README.md`` for the difficulty model and regeneration steps. -""" diff --git a/python_pkg/byox_ladder/build_pages.py b/python_pkg/byox_ladder/build_pages.py deleted file mode 100755 index 05a49f93..00000000 --- a/python_pkg/byox_ladder/build_pages.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Build the standalone HTML ladder pages from templates and parsed guide data. - -Produces two self-contained, dependency-free pages next to this script: - -* ``guide-ladder.html`` — every individual guide, built by injecting - ``guides.json`` (written by ``parse_guides.py``) into the guide template. -* ``category-ladder.html`` — the 30-category ladder, a static template. - -Both are wrapped in a minimal HTML document skeleton so they open directly via -``file://`` with no server, fonts, or network access. Run ``make build`` to -regenerate the whole pipeline. -""" - -from __future__ import annotations - -import json -import logging -from pathlib import Path - -logger = logging.getLogger(__name__) - -HERE = Path(__file__).resolve().parent -TEMPLATES = HERE / "templates" -GUIDES_JSON = HERE / "guides.json" - -# Placeholder in the guide template that the inlined dataset replaces. -MARKER = "/*__GUIDES__*/" - -# Minimal, valid document skeleton added around the published page content -# (claude.ai injects an equivalent wrapper; local files need their own). -SKELETON_HEAD = ( - "\n" - '\n' - "\n" - '\n' - '\n' -) -SKELETON_TAIL = "\n\n" - - -def standalone(content: str) -> str: - """Wrap page content in a minimal HTML document skeleton.""" - return SKELETON_HEAD + content + SKELETON_TAIL - - -def build_guide_page() -> str: - """Inject ``guides.json`` into the guide template; return standalone HTML.""" - guides = json.loads(GUIDES_JSON.read_text(encoding="utf-8")) - # Compact payload; guard any " str: - """Return the standalone category ladder (static content, no data).""" - content = (TEMPLATES / "category.html").read_text(encoding="utf-8") - return standalone(content) - - -def main() -> int: - """Write both standalone ladder pages next to this script; return an exit code.""" - if not GUIDES_JSON.exists(): - logger.error( - "missing %s — run parse_guides.py first (or make build)", GUIDES_JSON - ) - return 1 - (HERE / "guide-ladder.html").write_text(build_guide_page(), encoding="utf-8") - (HERE / "category-ladder.html").write_text(build_category_page(), encoding="utf-8") - logger.info("wrote guide-ladder.html and category-ladder.html") - return 0 - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, format="%(message)s") - raise SystemExit(main()) diff --git a/python_pkg/byox_ladder/category-ladder.html b/python_pkg/byox_ladder/category-ladder.html deleted file mode 100644 index 72135767..00000000 --- a/python_pkg/byox_ladder/category-ladder.html +++ /dev/null @@ -1,363 +0,0 @@ - - - - - -Build Your Own X — Difficulty Ladder - - -
-
-

codecrafters-io / build-your-own-x

-

The Build-Your-Own-X Difficulty Ladder

-

All 30 project categories from the - build-your-own-x - collection, ranked easiest → hardest and grouped into four tiers. - Sort or filter, then follow a card to its tutorials.

- -
-
PrerequisitesDomain knowledge a beginner must bring — crypto, linear algebra, OS internals.
-
ScopeHow much code a working toy version actually needs.
-
GuidanceWhether a canonical step-by-step tutorial exists to lean on.
-
-
- -
- - -
-
-
-
- - -
- - - - diff --git a/python_pkg/byox_ladder/guide-ladder.html b/python_pkg/byox_ladder/guide-ladder.html deleted file mode 100644 index c445e4e5..00000000 --- a/python_pkg/byox_ladder/guide-ladder.html +++ /dev/null @@ -1,320 +0,0 @@ - - - - - -Build Your Own X — Guide Ladder - - -
-
-

codecrafters-io / build-your-own-x

-

The Build-Your-Own-X Guide Ladder

-

Every individual tutorial from the - build-your-own-x - collection — 0 guides across 30 categories — placed on a difficulty - ladder. Search, filter by tier / language / category, and click any guide to open it.

-

How difficulty is assigned: each guide inherits its category's tier - (a Regex-Engine guide is easier than an OS guide, whatever the language). Language only nudges - the order within a tier — lower-level languages (C, Rust, Asm) sit a little higher than - Python or JS. The Unsorted group is the repo's own uncategorized bucket; its guides span - all levels, so they aren't ranked.

-
- -
- - -
-
- - -
- - - - diff --git a/python_pkg/byox_ladder/parse_guides.py b/python_pkg/byox_ladder/parse_guides.py deleted file mode 100755 index a5d75c7b..00000000 --- a/python_pkg/byox_ladder/parse_guides.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -"""Parse the build-your-own-x README into a ranked, tiered list of guides. - -Reads a local copy of the upstream README (``byox-readme.md`` — fetch it with the -command documented in this folder's ``README.md``, or run ``make build``) and -writes ``guides.json``. - -Difficulty model: every guide inherits its category's tier — the dominant -signal — while its language only nudges the ordering *within* that tier -(lower-level languages such as C or Rust sort a little harder than Python or -JavaScript). See ``README.md`` for the full rationale. -""" - -from __future__ import annotations - -import json -import logging -from pathlib import Path -import re - -logger = logging.getLogger(__name__) - -HERE = Path(__file__).resolve().parent -README = HERE / "byox-readme.md" -OUTPUT = HERE / "guides.json" - -# Category display name -> (global difficulty rank 1..30, tier). The rank order -# mirrors the approved per-category ladder; keys match the exact backtick names -# used in the README headers. -CATEGORIES: dict[str, tuple[int, str]] = { - "Command-Line Tool": (1, "beginner"), - "Bot": (2, "beginner"), - "Template Engine": (3, "beginner"), - "Text Editor": (4, "beginner"), - "Git": (5, "beginner"), - "Shell": (6, "intermediate"), - "Web Server": (7, "intermediate"), - "Search Engine": (8, "intermediate"), - "BitTorrent Client": (9, "intermediate"), - "Front-end Framework / Library": (10, "intermediate"), - "Regex Engine": (11, "intermediate"), - "Game": (12, "intermediate"), - "Programming Language": (13, "advanced"), - "Emulator / Virtual Machine": (14, "advanced"), - "Neural Network": (15, "advanced"), - "Memory Allocator": (16, "advanced"), - "Blockchain / Cryptocurrency": (17, "advanced"), - "Database": (18, "advanced"), - "3D Renderer": (19, "advanced"), - "Physics Engine": (20, "advanced"), - "Docker": (21, "advanced"), - "Voxel Engine": (22, "advanced"), - "Network Stack": (23, "expert"), - "AI Model": (24, "expert"), - "Visual Recognition System": (25, "expert"), - "Augmented Reality": (26, "expert"), - "Web Browser": (27, "expert"), - "Processor": (28, "expert"), - "Operating System": (29, "expert"), - "Distributed Systems": (30, "expert"), -} - -# Rank used for the repo's "Uncategorized" bucket, sorted after every real tier. -UNSORTED_RANK = 99 - -# Per-language ordering nudge (0.0 = high-level, higher = lower-level / more -# effort). Only orders guides *within* a category; it never crosses a tier. -LANG_EFFORT: dict[str, float] = { - "assembly": 0.9, - "asm": 0.9, - "c": 0.8, - "c++": 0.8, - "cpp": 0.8, - "rust": 0.75, - "zig": 0.75, - "d": 0.7, - "nim": 0.6, - "haskell": 0.6, - "ocaml": 0.6, - "go": 0.55, - "scala": 0.55, - "c#": 0.5, - "java": 0.5, - "kotlin": 0.5, - "swift": 0.5, - "f#": 0.5, - "clojure": 0.45, - "elixir": 0.4, - "shell": 0.4, - "bash": 0.4, - "r": 0.4, - "typescript": 0.35, - "php": 0.35, - "perl": 0.35, - "dart": 0.35, - "javascript": 0.3, - "node.js": 0.3, - "node": 0.3, - "ruby": 0.3, - "python": 0.3, - "lua": 0.3, - "any": 0.25, - "pseudocode": 0.2, -} - -# Default nudge for a language not present in ``LANG_EFFORT``. -DEFAULT_EFFORT = 0.5 - -HEADER_RE = re.compile(r"^####\s+Build your own\s+`([^`]+)`") -UNCAT_RE = re.compile(r"^####\s+Uncategorized") -GUIDE_RE = re.compile(r"^\*\s+\[(.+)\]\((\S+?)\)(.*)$") -LABEL_RE = re.compile(r"^\*\*(.+?)\*\*:\s*(.*)$") - - -def split_langs(raw: str) -> list[str]: - """Split a ``C# / TypeScript`` language blob into a clean list of names.""" - parts = re.split(r"[/,&]| or ", raw) - names = [part.strip().strip("*").strip() for part in parts] - cleaned = [name for name in names if name] - return cleaned or ["Various"] - - -def lang_effort(langs: list[str]) -> float: - """Return the easiest listed language's nudge (you pick your best one).""" - values = [LANG_EFFORT.get(lang.lower(), DEFAULT_EFFORT) for lang in langs] - return min(values) if values else DEFAULT_EFFORT - - -def clean_title(rest: str) -> str: - """Strip surrounding markdown italics/emphasis from a guide title.""" - title = rest.strip() - title = re.sub(r"^_+", "", title) - title = re.sub(r"_+$", "", title) - return title.strip().strip("*").strip() - - -def parse_guide_line(line: str, category: str) -> dict[str, object] | None: - """Parse one ``* [**Lang**: _Title_](url)`` line into a guide record.""" - match = GUIDE_RE.match(line.strip()) - if match is None: - return None - label, url, trailer = match.group(1), match.group(2), match.group(3) - label_match = LABEL_RE.match(label) - if label_match is not None: - langs = split_langs(label_match.group(1)) - title = clean_title(label_match.group(2)) - else: - langs = ["Various"] - title = clean_title(label) - rank, tier = CATEGORIES.get(category, (UNSORTED_RANK, "unsorted")) - return { - "title": title, - "url": url, - "langs": langs, - "category": category, - "cat_rank": rank, - "tier": tier, - "video": "[video]" in trailer.lower(), - "score": round(rank * 10 + lang_effort(langs), 2), - } - - -def parse_readme(text: str) -> list[dict[str, object]]: - """Parse the whole README text into a score-sorted list of guides.""" - guides: list[dict[str, object]] = [] - current: str | None = None - for line in text.splitlines(): - header = HEADER_RE.match(line) - if header is not None: - current = header.group(1).strip() - continue - if UNCAT_RE.match(line): - current = "Uncategorized" - continue - if current is None: - continue - guide = parse_guide_line(line, current) - if guide is not None: - guides.append(guide) - guides.sort(key=lambda item: (item["score"], str(item["title"]).lower())) - return guides - - -def main() -> int: - """Parse ``byox-readme.md`` and write ``guides.json``; return an exit code.""" - if not README.exists(): - logger.error("missing %s — fetch it first (see README.md / make build)", README) - return 1 - guides = parse_readme(README.read_text(encoding="utf-8")) - OUTPUT.write_text(json.dumps(guides, indent=1), encoding="utf-8") - logger.info("wrote %s (%d guides)", OUTPUT.name, len(guides)) - return 0 - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, format="%(message)s") - raise SystemExit(main()) diff --git a/python_pkg/byox_ladder/templates/category.html b/python_pkg/byox_ladder/templates/category.html deleted file mode 100644 index cfbfbdc1..00000000 --- a/python_pkg/byox_ladder/templates/category.html +++ /dev/null @@ -1,356 +0,0 @@ -Build Your Own X — Difficulty Ladder - - -
-
-

codecrafters-io / build-your-own-x

-

The Build-Your-Own-X Difficulty Ladder

-

All 30 project categories from the - build-your-own-x - collection, ranked easiest → hardest and grouped into four tiers. - Sort or filter, then follow a card to its tutorials.

- -
-
PrerequisitesDomain knowledge a beginner must bring — crypto, linear algebra, OS internals.
-
ScopeHow much code a working toy version actually needs.
-
GuidanceWhether a canonical step-by-step tutorial exists to lean on.
-
-
- -
- - -
-
-
-
- - -
- - diff --git a/python_pkg/byox_ladder/templates/guide.template.html b/python_pkg/byox_ladder/templates/guide.template.html deleted file mode 100644 index 5f26214e..00000000 --- a/python_pkg/byox_ladder/templates/guide.template.html +++ /dev/null @@ -1,313 +0,0 @@ -Build Your Own X — Guide Ladder - - -
-
-

codecrafters-io / build-your-own-x

-

The Build-Your-Own-X Guide Ladder

-

Every individual tutorial from the - build-your-own-x - collection — 0 guides across 30 categories — placed on a difficulty - ladder. Search, filter by tier / language / category, and click any guide to open it.

-

How difficulty is assigned: each guide inherits its category's tier - (a Regex-Engine guide is easier than an OS guide, whatever the language). Language only nudges - the order within a tier — lower-level languages (C, Rust, Asm) sit a little higher than - Python or JS. The Unsorted group is the repo's own uncategorized bucket; its guides span - all levels, so they aren't ranked.

-
- -
- - -
-
- - -
- - diff --git a/python_pkg/byox_ladder/tests/__init__.py b/python_pkg/byox_ladder/tests/__init__.py deleted file mode 100644 index 0d170a53..00000000 --- a/python_pkg/byox_ladder/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the byox_ladder package.""" diff --git a/python_pkg/byox_ladder/tests/test_build_pages.py b/python_pkg/byox_ladder/tests/test_build_pages.py deleted file mode 100644 index 4a54a2e5..00000000 --- a/python_pkg/byox_ladder/tests/test_build_pages.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Unit tests for the standalone page builder.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from python_pkg.byox_ladder.build_pages import ( - build_category_page, - build_guide_page, - main, - standalone, -) - -if TYPE_CHECKING: - from pathlib import Path - - -def _write(dirpath: Path, name: str, body: str) -> None: - (dirpath / name).write_text(body, encoding="utf-8") - - -def test_standalone_wraps_content() -> None: - out = standalone("

hi

") - assert out.startswith("") - assert "

hi

" in out - assert out.rstrip().endswith("") - - -def test_build_guide_page_injects_data( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - templates = tmp_path / "templates" - templates.mkdir() - _write( - templates, - "guide.template.html", - "x", - ) - guides_json = tmp_path / "guides.json" - guides_json.write_text('[{"title": "A"}]', encoding="utf-8") - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates) - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json) - html = build_guide_page() - assert "const GUIDES = [" in html - assert html.startswith("") - - -def test_build_guide_page_missing_marker_raises( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - templates = tmp_path / "templates" - templates.mkdir() - _write(templates, "guide.template.html", "no marker") - guides_json = tmp_path / "guides.json" - guides_json.write_text("[]", encoding="utf-8") - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates) - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json) - with pytest.raises(ValueError, match="marker"): - build_guide_page() - - -def test_build_category_page_wraps_template( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - templates = tmp_path / "templates" - templates.mkdir() - _write(templates, "category.html", "cat") - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates) - html = build_category_page() - assert "cat" in html - assert html.startswith("") - - -def test_main_missing_guides_returns_error( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr( - "python_pkg.byox_ladder.build_pages.GUIDES_JSON", tmp_path / "nope.json" - ) - assert main() == 1 - - -def test_main_writes_both_pages( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - templates = tmp_path / "templates" - templates.mkdir() - _write(templates, "guide.template.html", "") - _write(templates, "category.html", "cat") - guides_json = tmp_path / "guides.json" - guides_json.write_text("[]", encoding="utf-8") - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates) - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json) - monkeypatch.setattr("python_pkg.byox_ladder.build_pages.HERE", tmp_path) - assert main() == 0 - assert (tmp_path / "guide-ladder.html").exists() - assert (tmp_path / "category-ladder.html").exists() diff --git a/python_pkg/byox_ladder/tests/test_parse_guides.py b/python_pkg/byox_ladder/tests/test_parse_guides.py deleted file mode 100644 index 167a6f99..00000000 --- a/python_pkg/byox_ladder/tests/test_parse_guides.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Unit tests for the guide parser and its difficulty model.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from python_pkg.byox_ladder.parse_guides import ( - DEFAULT_EFFORT, - clean_title, - lang_effort, - main, - parse_guide_line, - parse_readme, - split_langs, -) - -if TYPE_CHECKING: - from pathlib import Path - - import pytest - -# A tiny README covering a real category, the Uncategorized bucket, a -# before-any-header line, and a non-guide line under a category. -README_SAMPLE = """intro line before any header -#### Build your own `Git` -* [**Go**: _Git in Go_](https://example.com/git) -not a guide line under a category -#### Uncategorized -* [**C++**: _Linux Debugger_](https://example.com/dbg) -""" - - -def test_split_langs_multiple_separators() -> None: - assert split_langs("C# / TypeScript & JavaScript") == [ - "C#", - "TypeScript", - "JavaScript", - ] - - -def test_split_langs_or_separator() -> None: - assert split_langs("Python or Go") == ["Python", "Go"] - - -def test_split_langs_empty_falls_back_to_various() -> None: - assert split_langs("**") == ["Various"] - - -def test_lang_effort_prefers_easiest_language() -> None: - assert lang_effort(["C", "Python"]) == 0.3 - - -def test_lang_effort_unknown_language_uses_default() -> None: - assert lang_effort(["Cobol"]) == DEFAULT_EFFORT - - -def test_lang_effort_empty_uses_default() -> None: - assert lang_effort([]) == DEFAULT_EFFORT - - -def test_clean_title_strips_emphasis() -> None: - assert clean_title("_Hello World_") == "Hello World" - - -def test_parse_guide_line_full_record() -> None: - line = "* [**Go**: _Build X_](https://example.com/x) [video]" - guide = parse_guide_line(line, "Git") - assert guide is not None - assert guide["title"] == "Build X" - assert guide["langs"] == ["Go"] - assert guide["category"] == "Git" - assert guide["tier"] == "beginner" - assert guide["video"] is True - - -def test_parse_guide_line_without_language_prefix() -> None: - guide = parse_guide_line("* [Plain title](https://example.com)", "Git") - assert guide is not None - assert guide["langs"] == ["Various"] - assert guide["title"] == "Plain title" - assert guide["video"] is False - - -def test_parse_guide_line_unknown_category_is_unsorted() -> None: - guide = parse_guide_line("* [**C**: _Thing_](https://example.com)", "Uncategorized") - assert guide is not None - assert guide["tier"] == "unsorted" - assert guide["cat_rank"] == 99 - - -def test_parse_guide_line_non_guide_returns_none() -> None: - assert parse_guide_line("not a bullet", "Git") is None - - -def test_parse_readme_groups_and_sorts_by_difficulty() -> None: - guides = parse_readme(README_SAMPLE) - titles = [guide["title"] for guide in guides] - assert "Git in Go" in titles - assert "Linux Debugger" in titles - # Git (rank 5) must sort ahead of the Uncategorized bucket (rank 99). - assert titles.index("Git in Go") < titles.index("Linux Debugger") - - -def test_parse_readme_ignores_lines_before_any_header() -> None: - assert parse_readme("* [**Go**: _Orphan_](https://example.com)\n") == [] - - -def test_main_missing_readme_returns_error( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr( - "python_pkg.byox_ladder.parse_guides.README", tmp_path / "nope.md" - ) - monkeypatch.setattr( - "python_pkg.byox_ladder.parse_guides.OUTPUT", tmp_path / "out.json" - ) - assert main() == 1 - - -def test_main_writes_guides_json( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - readme = tmp_path / "byox-readme.md" - readme.write_text(README_SAMPLE, encoding="utf-8") - output = tmp_path / "guides.json" - monkeypatch.setattr("python_pkg.byox_ladder.parse_guides.README", readme) - monkeypatch.setattr("python_pkg.byox_ladder.parse_guides.OUTPUT", output) - assert main() == 0 - assert output.exists()