From 3597f20224092ba12ca59e8aee3425483e76115b Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Sun, 20 Sep 2026 17:03:26 +0300 Subject: [PATCH 1/4] Give every page something to show when it is shared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the two landings carried Open Graph tags, hand-written, and neither had an image. The other fifty-four pages had none at all: shared anywhere, a module produced a bare link with no title and no card. That is the wrong way round. The shareable unit of a course is a module -- "вот модуль про дофамин и TD-обучение" -- and the module was the one thing with nothing to show. main.html emits the tags for every page from its own title and description, in a `social_meta` block so the landings can replace them rather than append: appending would have emitted og:title twice and left the scraper to choose. `page` is None while the theme renders the 404, so every access is guarded -- without that the build dies with "'None' has no attribute 'meta'" and does not say which template. The card is the one the organisation's generator already draws for lemma. Its palette is not a coincidence: #0a0b0e, #f8f9fb and #d8b678 are the site's own ink, paper and gold, so the card and the page a reader lands on are the same design. twitter:card goes from summary to summary_large_image, because a 1280×640 card shown as a thumbnail is a wasted card. --- docs/assets/og.png | Bin 0 -> 61656 bytes overrides/home.en.html | 11 ++++++++++- overrides/home.html | 13 ++++++++++++- overrides/main.html | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 docs/assets/og.png create mode 100644 overrides/main.html diff --git a/docs/assets/og.png b/docs/assets/og.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ef4c9fd21913d99f8c552bf777c262009e49fd GIT binary patch literal 61656 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%0D+V9$?;Uf&#iX;75atN(QUkbrkD)YKKF)=VmK? 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 literal 0 HcmV?d00001 diff --git a/overrides/home.en.html b/overrides/home.en.html index 90b779e..c45ea46 100644 --- a/overrides/home.en.html +++ b/overrides/home.en.html @@ -9,11 +9,20 @@ {% block extrahead %} {{ super() }} +{% endblock %} + +{# Replaces the generic tags from main.html rather than adding to them. #} +{% block social_meta %} + - + + + + + {% endblock %} {% block tabs %} diff --git a/overrides/home.html b/overrides/home.html index cf6ee6f..f301ef9 100644 --- a/overrides/home.html +++ b/overrides/home.html @@ -11,11 +11,22 @@ {# The content block below is empty on purpose; without this the theme still reserves a padded, empty article between the landing and the footer. #} +{% endblock %} + +{# Replaces the generic tags from main.html rather than adding to them: this + page's description is written for a reader deciding whether to start, which + a generic one cannot be. #} +{% block social_meta %} + - + + + + + {% endblock %} {% block tabs %} diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..4158d33 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{# + Open Graph on every page, not only on the two landings. + + The theme emits none, and the landings carried hand-written tags, so all + fifty-four module pages shared as a bare link: no title, no description, no + image. The shareable unit of a course is a module -- "вот модуль про дофамин + и TD-обучение" -- and that was the one thing with nothing to show. + + `social_meta` is its own block so a landing can replace these rather than + append to them: appending would emit og:title twice and leave the scraper to + pick. +#} + +{% block extrahead %} + {{ super() }} + + {% block social_meta %} + {#- `page` is None while the theme renders the 404, so every access here is + guarded. Without that the build dies with "'None' has no attribute + 'meta'" and mkdocs does not say which template. -#} + {%- set page_title = (page.title if page else None) | default(config.site_name, true) -%} + {%- set page_description = (page.meta.description if page and page.meta else None) + | default(config.site_description, true) -%} + {%- set page_url = (page.canonical_url if page else config.site_url) -%} + + + + + + + {# summary_large_image, not summary: the card is 1280×640 and a small + thumbnail wastes it. #} + + + + + {% endblock %} +{% endblock %} From 511a17f5b9f85bd1c61bfd4400b1d46781c796e8 Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Sun, 20 Sep 2026 18:26:31 +0300 Subject: [PATCH 2/4] Keep the cards from rotting quietly The tags from the previous commit come out of a template, not out of the pages, and every way they break is a way the build still passes. `page` is empty while the theme renders the 404. Jinja does not raise there -- it substitutes an empty string -- so one unguarded access takes og:title off all sixty-five pages and `mkdocs build --strict` reports success. Rename the card and og:image keeps pointing at the old name. Move `social_meta` so a landing appends instead of replacing and og:title comes out twice, leaving the scraper to choose. None of this is a link, so the link checker is blind to it, and none of it is visible on the site itself: a card is only ever seen somewhere else, by someone who is not us. check_social.py reads the built directory and refuses all seven: a missing, empty or duplicated tag, an og:image that is relative or points at a file the build does not contain, an og:url shared by two pages, twitter:card downgraded from summary_large_image, and one title repeated across the site -- which is what "the template stopped seeing page" looks like from the outside. Each of the seven was checked by breaking a copy of the build and confirming the script fails. The two-page allowance on repeated titles is deliberate: the ru and en landings are both called lemma, and that is correct. Standard library only, and it runs against the directory mkdocs has just built, so the site job gains a step and nothing to install. --- .github/workflows/ci.yml | 7 +++ scripts/check_social.py | 113 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 scripts/check_social.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ad2702..5724e5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,13 @@ jobs: - name: Модули ведут к своим ноутбукам run: python scripts/check_modules.py + # Теги Open Graph отдаёт шаблон, а не страница: сборка проходит и с + # пустым заголовком (при 404 `page` пуста), и с картинкой, которой уже + # нет, и с заголовком, выведенным дважды. Проверяется по собранному + # каталогу, поэтому шаг идёт после mkdocs build. + - name: У каждой страницы есть что показать, когда ей делятся + run: python scripts/check_social.py site + ci: name: CI runs-on: ubuntu-latest diff --git a/scripts/check_social.py b/scripts/check_social.py new file mode 100644 index 0000000..53a6045 --- /dev/null +++ b/scripts/check_social.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Проверки собранного сайта: каждая страница что-то показывает, когда ей делятся. + +Теги Open Graph отдаёт шаблон `overrides/main.html`, а не сама страница, и +ошибиться в нём можно тихо. `page` пуста при отрисовке 404 — jinja в этом месте +не падает, а подставляет пустую строку, и весь сайт уезжает с пустым og:title. +Переименовали картинку — og:image остаётся, но ведёт в никуда. Перенесли блок +`social_meta` так, что лендинг не заменяет теги, а дописывает, — og:title +выходит дважды, и какой из них возьмёт сборщик карточки, решает сборщик. + +Ничего из этого не заметит ни `mkdocs build --strict` (шаблон отработал), +ни проверка ссылок (теги — не ссылки), ни глаз: карточку видно только там, +куда ссылку вставили. + +Только стандартная библиотека, и работает по уже собранному каталогу: + + mkdocs build --strict + python scripts/check_social.py # по умолчанию ./site + python scripts/check_social.py <каталог> +""" + +from __future__ import annotations + +import pathlib +import re +import sys +from collections import Counter + +SITE_URL = "https://drobyshevdev.github.io/lemma/" + +# Страницы без собственного адреса: 404 отдаётся с любого пути, поэтому +# требовать от неё уникальный og:url бессмысленно. +NO_OWN_URL = {"404.html", "en/404.html"} + + +def _meta(html: str, attr: str, name: str) -> list[str]: + return re.findall(rf' int: + site = pathlib.Path(argv[1] if len(argv) > 1 else "site") + if not site.is_dir(): + print(f" каталога со сборкой нет: {site} — сначала mkdocs build", file=sys.stderr) + return 2 + + pages = sorted(site.rglob("*.html")) + if not pages: + print(f" в {site} нет ни одной страницы", file=sys.stderr) + return 2 + + problems: list[str] = [] + titles: Counter[str] = Counter() + urls: Counter[str] = Counter() + + for page in pages: + rel = page.relative_to(site).as_posix() + html = page.read_text(encoding="utf-8", errors="replace") + + for attr, name in (("property", "og:title"), ("property", "og:image"), + ("property", "og:url"), ("name", "twitter:card")): + found = _meta(html, attr, name) + if not found: + problems.append(f"{rel}: нет {name}") + elif len(found) > 1: + # Ровно то, ради чего social_meta сделан отдельным блоком. + problems.append(f"{rel}: {name} выведен {len(found)} раза — {found}") + elif not found[0].strip(): + problems.append(f"{rel}: {name} пуст") + + title = _meta(html, "property", "og:title") + if len(title) == 1 and title[0].strip(): + titles[title[0]] += 1 + + url = _meta(html, "property", "og:url") + if len(url) == 1 and url[0].strip() and rel not in NO_OWN_URL: + urls[url[0]] += 1 + + card = _meta(html, "name", "twitter:card") + if card and card[0] != "summary_large_image": + problems.append( + f"{rel}: twitter:card = {card[0]}, а карточка 1280×640 — " + "summary_large_image, иначе её покажут миниатюрой" + ) + + for image in _meta(html, "property", "og:image"): + if not image.startswith("https://"): + problems.append(f"{rel}: og:image не абсолютен ({image}) — сборщику карточки нужен адрес") + elif image.startswith(SITE_URL): + target = site / image[len(SITE_URL):] + if not target.is_file(): + problems.append(f"{rel}: og:image ведёт на {image}, а файла в сборке нет") + + # Одинаковый заголовок на всех — признак того, что шаблон не видит page и + # подставляет config.site_name. Сборка при этом проходит. + for title, count in titles.items(): + if count > 2: # два лендинга, ru и en, законно зовутся одинаково + problems.append(f"og:title «{title}» повторяется на {count} страницах — шаблон не видит page?") + for url, count in urls.items(): + if count > 1: + problems.append(f"og:url {url} повторяется на {count} страницах") + + for problem in problems: + print(f" {problem}") + if problems: + print(f"\n проблем: {len(problems)}", file=sys.stderr) + return 1 + + print(f" {len(pages)} страниц, у каждой свой заголовок и адрес, картинка одна и она на месте.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) From 99fa90d272020c825da5ce03fd6a73aa9fb155c9 Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Sun, 20 Sep 2026 18:33:02 +0300 Subject: [PATCH 3/4] Count the course once, instead of nine times by hand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lemma says "двадцать семь модулей в семи частях" in nine places -- in digits, in Russian words and in English words: the badge, README prose twice, both programmes, both landings, CITATION.cff and .zenodo.json. None of the nine knows how many modules there are. Adding one means hitting nine files and never slipping, and the first slip survives until somebody counts by hand. check_counts.py counts once -- docs/modules/, notebooks/, and the programme's own tables -- and holds the nine to it. It also sums the "Время" column, which is where the two totals come from: "около 50 недель" and the figure given for parts I-II. Nobody re-adds a column while editing one row. A claim that has gone missing counts as a discrepancy too. Rewrite the sentence and the pattern stops matching, the check stops checking, and it says nothing about that -- which is the failure mode of this whole kind of test. So an unfound claim is reported exactly like a wrong number. It found two things on its first run. Parts I-II were described as 8 weeks. Their own tables give 12: part I is 1+2+2+2, part II is 2+2+1. The neighbouring total is right -- the tables sum to 51 and the text says about 50 -- so 8 is a slip, not a different way of counting, and the same slip is in both languages. Changed to 12 in both. If 8 was meant as something else, this is the line to push back on. The second was mine: "в семь частях" instead of "в семи частях". A numeral has a case, and a check that builds Russian out of a number has to decline it, or it fails on correct text. RU_PREP_IRREGULAR names the forms the rule misses -- including восьми, which is not восеми. Fourteen ways of drifting were tried against a copy: badge, each landing, each citation file, a rewritten sentence, a module dropped from the navigation, a deleted notebook, a missing translation, a duplicated number, a changed duration, the two languages disagreeing, and .zenodo.json left unparseable. Thirteen were caught as written; the fourteenth raised instead of reporting, and now reports. Standard library, no build needed. --- .github/workflows/ci.yml | 7 ++ docs/programme.en.md | 2 +- docs/programme.md | 2 +- scripts/check_counts.py | 239 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 scripts/check_counts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5724e5f..e509959 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,13 @@ jobs: - name: Модули ведут к своим ноутбукам run: python scripts/check_modules.py + # Курс девять раз называет себя «двадцать семь модулей в семи частях» -- + # цифрами, русскими словами и английскими. Ни одно из этих мест не знает, + # сколько модулей на самом деле, и разойтись они могут молча. Считается + # здесь один раз, по каталогам и таблицам программы. + - name: Курс говорит о себе правду + run: python scripts/check_counts.py + # Теги Open Graph отдаёт шаблон, а не страница: сборка проходит и с # пустым заголовком (при 404 `page` пуста), и с картинкой, которой уже # нет, и с заголовком, выведенным дважды. Проверяется по собранному diff --git a/docs/programme.en.md b/docs/programme.en.md index 821386c..e5477b3 100644 --- a/docs/programme.en.md +++ b/docs/programme.en.md @@ -97,5 +97,5 @@ How to choose it, do it and write it up — a full walkthrough with a checklist ## In total **About 50 weeks** at ten hours a week — a year at an unhurried pace, or half a year at a -dense one. Parts I–II (8 weeks) make sense on their own: even if you stop there, you will read +dense one. Parts I–II (12 weeks) make sense on their own: even if you stop there, you will read papers more carefully than most of the people who write them. diff --git a/docs/programme.md b/docs/programme.md index 5e55c02..5a4b6ae 100644 --- a/docs/programme.md +++ b/docs/programme.md @@ -98,5 +98,5 @@ ## Итого **Около 50 недель** при десяти часах в неделю — год неспешно или полгода плотно. Части -I–II (8 недель) имеют смысл сами по себе: даже если вы остановитесь на них, вы будете +I–II (12 недель) имеют смысл сами по себе: даже если вы остановитесь на них, вы будете читать статьи внимательнее, чем большинство тех, кто их пишет. diff --git a/scripts/check_counts.py b/scripts/check_counts.py new file mode 100644 index 0000000..dd63d2a --- /dev/null +++ b/scripts/check_counts.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Числа, которые курс говорит о себе, сходятся с тем, из чего он состоит. + +Курс девять раз называет себя «двадцать семь модулей в семи частях» — цифрами, +русскими словами и английскими словами: в README, на двух лендингах, в двух +программах, в бейдже, в CITATION.cff и в .zenodo.json. Ни одно из этих мест не +знает, сколько модулей на самом деле. Добавить модуль — значит попасть в девять +файлов и ни разу не ошибиться, а первая же неправка живёт до тех пор, пока +кто-нибудь не пересчитает руками. + +Здесь считается ровно один раз — по `docs/modules/`, `notebooks/` и таблицам +программы, — а дальше каждое утверждение сверяется с этим счётом. + +Отдельно сверяются недели: программа обещает «около 50 недель» и «части I–II — +столько-то», и оба числа есть сумма колонки «Время» в её же таблицах. Такую +сумму никто не пересчитывает, правя одну строку. + +Пропавшее утверждение — тоже расхождение. Если фразу перепишут так, что шаблон +перестанет находиться, проверка перестанет проверять и промолчит об этом; +поэтому ненайденное место сообщается наравне с разошедшимся числом. + +Только стандартная библиотека. + + python scripts/check_counts.py +""" + +from __future__ import annotations + +import json +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent + +# Слова до сорока: курс из сорока модулей — уже другой курс, и падение проверки +# на сорок первом правильно, а не недосмотр. +RU_ONES = ["", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять", + "десять", "одиннадцать", "двенадцать", "тринадцать", "четырнадцать", "пятнадцать", + "шестнадцать", "семнадцать", "восемнадцать", "девятнадцать"] +RU_TENS = {20: "двадцать", 30: "тридцать", 40: "сорок"} +EN_ONES = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", + "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", + "sixteen", "seventeen", "eighteen", "nineteen"] +EN_TENS = {20: "twenty", 30: "thirty", 40: "forty"} + + +def ru_word(n: int) -> str: + if n < 20: + return RU_ONES[n] + tens, ones = divmod(n, 10) + return (RU_TENS[tens * 10] + (f" {RU_ONES[ones]}" if ones else "")).strip() + + +def en_word(n: int) -> str: + if n < 20: + return EN_ONES[n] + tens, ones = divmod(n, 10) + return EN_TENS[tens * 10] + (f"-{EN_ONES[ones]}" if ones else "") + + +#: Предложный падеж тех же числительных: «в семи частях», не «в семь частях». +#: Правило «мягкий знак на -и» ловит пять…двадцать и тридцать, но не первые +#: четыре, не восемь (восьми, а не «восеми») и не сорок — они названы отдельно. +RU_PREP_IRREGULAR = {1: "одном", 2: "двух", 3: "трёх", 4: "четырёх", 8: "восьми", + 40: "сорока"} + + +def ru_word_prep(n: int) -> str: + if n in RU_PREP_IRREGULAR: + return RU_PREP_IRREGULAR[n] + if n < 20 or n in RU_TENS: + return ru_word(n).removesuffix("ь") + "и" + tens, ones = divmod(n, 10) + return f"{ru_word_prep(tens * 10)} {ru_word_prep(ones)}" + + +def ru_plural(n: int, one: str, few: str, many: str) -> str: + """модуль / модуля / модулей — по последним цифрам, как в русском.""" + if 11 <= n % 100 <= 14: + return many + last = n % 10 + return one if last == 1 else few if 2 <= last <= 4 else many + + +def read(name: str) -> str: + return (ROOT / name).read_text(encoding="utf-8") + + +def parse_programme(name: str) -> tuple[list[tuple[str, list[int], float]], float]: + """Части программы: римская цифра, номера модулей, сумма недель.""" + parts: list[tuple[str, list[int], float]] = [] + inside = False + for line in read(f"docs/{name}").splitlines(): + head = re.match(r"^## (?:Часть|Part) ([IVX]+)\.", line) + if head: + parts.append((head.group(1), [], 0.0)) + inside = True + continue + if line.startswith("## "): + inside = False + row = re.match( + r"^\| (\d+) \| \[[^\]]+\]\(modules/[\w.-]+\.md\).*\| ([^|]+) \|\s*$", line + ) + if row and inside: + weeks = re.search(r"([\d.]+)", row.group(2)) + name_, numbers, total = parts[-1] + numbers.append(int(row.group(1))) + parts[-1] = (name_, numbers, total + (float(weeks.group(1)) if weeks else 0.0)) + return parts, sum(p[2] for p in parts) + + +def main() -> int: + problems: list[str] = [] + + def want(where: str, pattern: str, what: str) -> None: + """Сверить одно утверждение. Не найдено — тоже расхождение.""" + if re.search(pattern, read(where), re.M) is None: + problems.append(f"{where}: ждали {what} — не сходится или фразу переписали") + + # ---- то, из чего курс состоит ------------------------------------------- + ru_pages = sorted(p for p in (ROOT / "docs" / "modules").glob("*.md") + if not p.name.endswith(".en.md")) + en_pages = sorted((ROOT / "docs" / "modules").glob("*.en.md")) + notebooks = sorted((ROOT / "notebooks").glob("*.ipynb")) + n = len(ru_pages) + if not n: + print(" страниц модулей нет", file=sys.stderr) + return 2 + + if len(en_pages) != n: + problems.append(f"docs/modules: {n} страниц по-русски и {len(en_pages)} по-английски") + if len(notebooks) != n: + problems.append(f"notebooks: {len(notebooks)} ноутбуков на {n} модулей") + + ru_parts, ru_weeks = parse_programme("programme.md") + en_parts, en_weeks = parse_programme("programme.en.md") + parts = len(ru_parts) + numbered = [i for p in ru_parts for i in p[1]] + + if numbered != list(range(1, n + 1)): + problems.append( + f"docs/programme.md: модули пронумерованы {numbered}, а страниц {n} — " + "номер пропущен или задвоен" + ) + if [p[1] for p in en_parts] != [p[1] for p in ru_parts]: + problems.append("docs/programme.en.md: разбивка по частям не та же, что в русской") + if ru_weeks != en_weeks: + problems.append(f"недели расходятся между языками: {ru_weeks:g} и {en_weeks:g}") + + n_ru, n_en = ru_word(n), en_word(n) + p_ru, p_en = ru_word(parts), en_word(parts) + p_ru_prep = ru_word_prep(parts) + modules_ru = ru_plural(n, "модуль", "модуля", "модулей") + parts_ru = ru_plural(parts, "часть", "части", "частей") + parts_ru_prep = ru_plural(parts, "части", "частях", "частях") + + # ---- то, что курс о себе говорит ---------------------------------------- + want("README.md", rf"img\.shields\.io/badge/модулей-{n}-", f"бейдж модулей-{n}") + want("README.md", rf"(?i)все {n} {modules_ru}", f"«все {n} {modules_ru}»") + want("README.md", rf"(?i){n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}", + f"«{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}»") + want("README.md", rf"(?i)все {n_ru} {modules_ru} готовы", f"«все {n_ru} {modules_ru} готовы»") + + want("docs/programme.md", rf"(?i)^{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}\.", + f"«{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}.»") + want("docs/programme.en.md", rf"(?i)^{n_en} modules in {p_en} parts\.", + f"«{n_en} modules in {p_en} parts.»") + + want("overrides/home.html", rf"{n} {modules_ru}", f"«{n} {modules_ru}» в плашке") + want("overrides/home.html", rf"(?i)

{p_ru} {parts_ru}, {n_ru} {modules_ru}

", + f"заголовок «{p_ru} {parts_ru}, {n_ru} {modules_ru}»") + want("overrides/home.en.html", rf"{n} modules", f"«{n} modules» в плашке") + want("overrides/home.en.html", rf"(?i)

{p_en} parts, {n_en} modules

", + f"заголовок «{p_en} parts, {n_en} modules»") + + want("CITATION.cff", rf"(?i){n_en} modules in {p_en} parts", + f"«{n_en} modules in {p_en} parts»") + want(".zenodo.json", rf"(?i){n_en} modules in {p_en} parts", + f"«{n_en} modules in {p_en} parts»") + + nav = len(re.findall(r"modules/[\w.-]+\.md", read("mkdocs.yml"))) + if nav != n: + problems.append(f"mkdocs.yml: в навигации {nav} модулей, а страниц {n}") + + # ---- недели ------------------------------------------------------------- + # «Около 50» — округление, поэтому допуск; части I–II названы точно, + # поэтому точное сравнение. + for name, source, about, exact in ( + ("docs/programme.md", ru_parts, r"\*\*Около (\d+) недель\*\*", r"I–II \((\d+) недель\)"), + ("docs/programme.en.md", en_parts, r"\*\*About (\d+) weeks\*\*", r"I–II \((\d+) weeks\)"), + ): + text = read(name) + total = sum(p[2] for p in source) + head = sum(p[2] for p in source[:2]) + + found = re.search(about, text) + if found is None: + problems.append(f"{name}: не нашлось общее число недель") + elif abs(int(found.group(1)) - total) > 2: + problems.append( + f"{name}: обещано {found.group(1)} недель, в таблицах {total:g} — " + "это уже не округление" + ) + + found = re.search(exact, text) + if found is None: + problems.append(f"{name}: не нашлось число недель на части I–II") + elif int(found.group(1)) != head: + problems.append( + f"{name}: части I–II названы как {found.group(1)} недель, " + f"а их собственные таблицы дают {head:g}" + ) + + # .zenodo.json должен ещё и разбираться. Неразбираемый файл Zenodo просто + # не прочтёт, а релиз при этом выйдет — с записью по умолчанию и без + # авторов. Сообщением, а не исключением: падать трейсбэком там, где + # остальные расхождения читаются строкой, — плохой отчёт. + try: + json.loads(read(".zenodo.json")) + except json.JSONDecodeError as broken: + problems.append(f".zenodo.json: не разбирается как JSON — {broken}") + + for problem in problems: + print(f" {problem}") + if problems: + print(f"\n расхождений: {len(problems)}", file=sys.stderr) + return 1 + + weeks_ru = ru_plural(int(ru_weeks), "неделя", "недели", "недель") + print( + f" {n} {modules_ru} в {parts} {parts_ru_prep}, {len(notebooks)} ноутбуков, " + f"{ru_weeks:g} {weeks_ru} — и все девять мест, где курс это говорит, говорят то же." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8c74e80aa37d030b8af3a6be855208c930f264b7 Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Sun, 20 Sep 2026 18:36:31 +0300 Subject: [PATCH 4/4] Say on the front page that the course is in English too The site has been bilingual for a while -- twenty-seven modules in Russian and twenty-seven in English, the same notebook behind each -- and the repository never said so. GitHub's front page was Russian only, so an English-speaking arrival saw a Russian page about a course they can in fact read. README.en.md is the whole README, not a stub pointing at the site: someone who lands on GitHub decides there, from the badges, the layout and the table of what is done, and a stub asks them to click before they know whether it is worth clicking. Both files now carry the language line the other repositories use. Two claims were out of date rather than missing. CITATION.cff and .zenodo.json both said "Written in Russian", which stopped being true when the English build was finished; both now say Russian and English, both complete, and carry the keyword. .zenodo.json keeps `language: rus` -- the field takes one value and Russian is the source language. Flip it if the English side should be the one indexed. CITATION.cff gets no `languages` key. It reads like the place for this, but in CFF 1.2.0 `languages` exists only inside `definitions.reference`, not at the top level, and an unknown top-level key is how "Cite this repository" quietly stops rendering. README.en.md would otherwise be a tenth surface asserting "twenty-seven modules in seven parts" with nothing holding it to the count -- the exact problem the previous commit is about -- so its badge and its two spelled-out claims are checked like the rest. check_counts.py also stops saying "nine places". It counts them: sixteen assertions across eight files today. A script whose whole job is that nobody should hold a number in their head had no business holding one. --- .zenodo.json | 3 +- CITATION.cff | 5 +- README.en.md | 112 ++++++++++++++++++++++++++++++++++++++++ README.md | 5 ++ scripts/check_counts.py | 27 +++++++--- 5 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 README.en.md diff --git a/.zenodo.json b/.zenodo.json index 3144ab5..e37267d 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,7 +1,7 @@ { "upload_type": "lesson", "title": "lemma — a free course in machine learning, deep learning and reinforcement learning", - "description": "A complete roadmap through machine learning, neural networks, reinforcement learning and recommender systems: twenty-seven modules in seven parts, from the arithmetic of a mean to reproducing a recent paper. The central skill taught is checking a claim rather than launching a training run, so the first module covers baselines and confidence intervals before any machine learning at all. Written in Russian; the notebooks run on a CPU in seconds and are executed in CI on Linux and Windows.", + "description": "A complete roadmap through machine learning, neural networks, reinforcement learning and recommender systems: twenty-seven modules in seven parts, from the arithmetic of a mean to reproducing a recent paper. The central skill taught is checking a claim rather than launching a training run, so the first module covers baselines and confidence intervals before any machine learning at all. Written in Russian and English, both versions complete; the notebooks run on a CPU in seconds and are executed in CI on Linux and Windows.", "creators": [ { "name": "Drobyshev, Denis", @@ -19,6 +19,7 @@ "reproducibility", "research-methods", "russian", + "english", "jupyter-notebook" ], "access_right": "open", diff --git a/CITATION.cff b/CITATION.cff index ace3846..419cb2d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,8 +10,8 @@ abstract: >- the arithmetic of a mean to reproducing a recent paper. The central skill taught is checking a claim rather than launching a training run, so the first module covers baselines and confidence intervals before any machine learning - at all. Written in Russian; the notebooks run on a CPU in seconds and are - executed in CI on Linux and Windows. + at all. Written in Russian and English, both versions complete; the notebooks + run on a CPU in seconds and are executed in CI on Linux and Windows. authors: - family-names: Drobyshev given-names: Denis @@ -28,3 +28,4 @@ keywords: - reproducibility - research-methods - russian + - english diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..82eee5b --- /dev/null +++ b/README.en.md @@ -0,0 +1,112 @@ +# lemma + +[Русский](README.md) · **English** · [Read the course](https://drobyshevdev.github.io/lemma/en/) + +**A free course in machine learning, neural networks, reinforcement learning and +recommender systems — from nothing to reading and reproducing research.** + +[![Site](https://img.shields.io/badge/site-drobyshevdev.github.io/lemma-4f46e5)](https://drobyshevdev.github.io/lemma/en/) +[![CI](https://github.com/DrobyshevDev/lemma/actions/workflows/ci.yml/badge.svg)](https://github.com/DrobyshevDev/lemma/actions/workflows/ci.yml) +[![Modules](https://img.shields.io/badge/modules-27-8A2BE2.svg)](https://drobyshevdev.github.io/lemma/en/programme/) +[![Text: CC BY 4.0](https://img.shields.io/badge/text-CC%20BY%204.0-lightgrey.svg)](LICENSE-CONTENT) +[![Code: MIT](https://img.shields.io/badge/code-MIT-green.svg)](LICENSE) + +**[Read the course →](https://drobyshevdev.github.io/lemma/en/)** + +## What this is + +A roadmap through ML, DL and RL: what to learn, in what order, why, and how to tell that +you have actually learned it. Twenty-seven modules in seven parts, from the arithmetic of +a mean and a variance to reproducing a recent paper as a capstone. + +Free and open, in full. No sign-up, no "first module free", no 24-month instalment plan. + +A lemma is a statement proved not for its own sake but to prove the next one. The course +is built the same way: no module stands on its own, each one holds up what comes after. + +Both languages are complete — twenty-seven modules in Russian and twenty-seven in English, +the same notebook behind each. + +## What is different about it + +Most courses teach you to train models. This one teaches you to **check claims**. + +The field moves through papers, and the overwhelming majority of the improvements claimed +in them do not reproduce, dissolve under an honest comparison, or come from comparing a +tuned method against an untuned baseline. Someone who can train a model but cannot check +a claim cannot tell progress from noise — and builds on noise. + +So a module does not end with "we got accuracy 0.93" but with "we checked that the +improvement survives a change of random seed and a comparison against an honest baseline". +Module 1 is about exactly that, before any machine learning at all. + +The second difference is the tie to psychology where the tie is real: reinforcement +learning and behavioural psychology describe the same thing twice over, and recommender +systems are applied psychology of attention. + +The third is that the course is written by the people who build the tools it uses. Where +run tracking is needed, that is `mlango`; where an agent loop with a readable trace is +needed, `glia`; where a trained policy has to be compared against a classical baseline, +`decisionrl`, which ships that baseline with every problem. None of the libraries is +required: everywhere the course also shows how to do the same thing by hand. + +## Layout + +``` +docs/ + index.en.md landing page (laid out in overrides/home.en.html) + programme.en.md all 27 modules + capstone.en.md the capstone: choosing a paper, reproducing it, writing it up + prerequisites.en.md what to know before starting + how-to-study.en.md how to study so that it works + modules/ modules, .md in Russian and .en.md in English + assets/theme.css a dark editorial theme over mkdocs-material +overrides/home.en.html the landing template +notebooks/ notebooks, one per module, shared by both languages +``` + +Notebooks run top to bottom with no edits and **on a CPU in reasonable time**. Where a full +run gives a different number, that is said outright. CI executes every notebook on Linux +and Windows: a reader whose notebook does not run does not have the course. + +## Running it + +From any module page, through the **"open in Colab"** link next to the notebook. There is +nothing to install: the course code imports only NumPy and Matplotlib, and Colab already +has both. A reader who cares about one module should not have to build an environment for it. + +Locally, if you would rather keep everything yourself: + +```bash +pip install -r requirements.txt +mkdocs serve # → http://127.0.0.1:8000 +jupyter lab notebooks/ +``` + +## State + +All twenty-seven modules are done — both language versions, a notebook for each, CI green. +The course can be taken end to end, from the first module to the capstone. + +| Part | Modules | State | +|---|---|---| +| I. How claims are checked | 1–4 | **complete** | +| II. Classical ML | 5–7 | **complete** | +| III. Neural networks | 8–11 | **complete** | +| IV. RL and psychology | 12–16 | **complete** | +| V. Recommender systems | 17–20 | **complete** | +| VI. Agents | 21–23 | **complete** | +| VII. Out to the frontier | 24–27 | **complete** | + +## Helping + +The most useful feedback is **"I got stuck here"**. If an explanation did not work, that is +a defect in the text, not in the reader, and it needs to be known about. Open an +[issue](https://github.com/DrobyshevDev/lemma/issues) naming the module and the place. + +Contribution rules — the [organisation's CONTRIBUTING.md](https://github.com/DrobyshevDev/.github/blob/master/CONTRIBUTING.md). + +## Licences + +Text and illustrations — [CC BY 4.0](LICENSE-CONTENT): take them, translate them, use them +in your own teaching, give credit. Code in the notebooks and scripts — [MIT](LICENSE). diff --git a/README.md b/README.md index 81b5102..28edcce 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # lemma +**Русский** · [English](README.en.md) · [Читать курс](https://drobyshevdev.github.io/lemma/) + **Бесплатный курс по машинному обучению, нейросетям, обучению с подкреплением и рекомендательным системам — с нуля и до умения читать и воспроизводить исследования.** @@ -24,6 +26,9 @@ следующее. Курс устроен так же: ни один модуль не стоит отдельно, каждый — опора для того, что идёт после. +Обе языковые версии полные — двадцать семь модулей по-русски и двадцать семь по-английски, +за каждым один и тот же ноутбук. + ## Чем отличается Большинство курсов учат обучать модели. Этот учит **проверять утверждения**. diff --git a/scripts/check_counts.py b/scripts/check_counts.py index dd63d2a..41f92c3 100644 --- a/scripts/check_counts.py +++ b/scripts/check_counts.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 """Числа, которые курс говорит о себе, сходятся с тем, из чего он состоит. -Курс девять раз называет себя «двадцать семь модулей в семи частях» — цифрами, -русскими словами и английскими словами: в README, на двух лендингах, в двух -программах, в бейдже, в CITATION.cff и в .zenodo.json. Ни одно из этих мест не -знает, сколько модулей на самом деле. Добавить модуль — значит попасть в девять -файлов и ни разу не ошибиться, а первая же неправка живёт до тех пор, пока -кто-нибудь не пересчитает руками. +Курс называет себя «двадцать семь модулей в семи частях» в обоих README, на +двух лендингах, в двух программах, в двух бейджах, в CITATION.cff и в +.zenodo.json — цифрами, русскими словами и английскими. Ни одно из этих мест не +знает, сколько модулей на самом деле. Добавить модуль — значит попасть в +каждое из них и ни разу не ошибиться, а первая же неправка живёт до тех пор, +пока кто-нибудь не пересчитает руками. + +Сколько мест проверено, скрипт говорит сам: число утверждений — тоже число, +которое ему незачем знать наизусть. Здесь считается ровно один раз — по `docs/modules/`, `notebooks/` и таблицам программы, — а дальше каждое утверждение сверяется с этим счётом. @@ -113,8 +116,11 @@ def parse_programme(name: str) -> tuple[list[tuple[str, list[int], float]], floa def main() -> int: problems: list[str] = [] + checked: list[str] = [] + def want(where: str, pattern: str, what: str) -> None: """Сверить одно утверждение. Не найдено — тоже расхождение.""" + checked.append(where) if re.search(pattern, read(where), re.M) is None: problems.append(f"{where}: ждали {what} — не сходится или фразу переписали") @@ -162,6 +168,12 @@ def want(where: str, pattern: str, what: str) -> None: f"«{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}»") want("README.md", rf"(?i)все {n_ru} {modules_ru} готовы", f"«все {n_ru} {modules_ru} готовы»") + want("README.en.md", rf"img\.shields\.io/badge/modules-{n}-", f"бейдж modules-{n}") + want("README.en.md", rf"(?i)all {n} modules", f"«all {n} modules»") + want("README.en.md", rf"(?i){n_en} modules in {p_en} parts", + f"«{n_en} modules in {p_en} parts»") + want("README.en.md", rf"(?i)all {n_en} modules are done", f"«all {n_en} modules are done»") + want("docs/programme.md", rf"(?i)^{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}\.", f"«{n_ru} {modules_ru} в {p_ru_prep} {parts_ru_prep}.»") want("docs/programme.en.md", rf"(?i)^{n_en} modules in {p_en} parts\.", @@ -230,7 +242,8 @@ def want(where: str, pattern: str, what: str) -> None: weeks_ru = ru_plural(int(ru_weeks), "неделя", "недели", "недель") print( f" {n} {modules_ru} в {parts} {parts_ru_prep}, {len(notebooks)} ноутбуков, " - f"{ru_weeks:g} {weeks_ru} — и все девять мест, где курс это говорит, говорят то же." + f"{ru_weeks:g} {weeks_ru} — и все {len(checked)} мест, где курс это говорит " + f"({len(set(checked))} файлов), говорят то же." ) return 0