Status: active
Личный статичный архив математических объектов, эссе и алгоритмических скетчей.
null/
├── index.html сетка карточек разделов
├── styles/main.css единственная таблица стилей
├── objects/ 134 объекта: числа, идеи, нотация, статистика
├── essays/ 110 эссе
├── visuals/ 36 интерактивных визуализаций
├── books/ 16 книг
├── music/ 10 альбомов
├── man.html руководство в формате man-page
├── map.html граф связей
├── find.html поиск
├── sitemap.xml карта сайта, генерируется
├── scripts/ update_counts.py, generate_sitemap.py,
│ update_meta.py, preflight.py
├── data/links.json граф: 313 узлов, 1710 рёбер
├── data/build-backlinks.js генератор блоков «упоминается в»
└── graph-health.py диагностика графа
Числа в man.html и index.html поддерживает scripts/update_counts.py —
править их руками не нужно.
scripts/update_counts.py про essays/index.html не знает вообще.
Оба числа секции — essay-section-count в заголовке и card-num
в карточке категории вверху страницы — правятся руками при добавлении
эссе. Забыть второе легко: расхождение видно на странице, но ничего
не ломает, и потому живёт долго.
Три числа рядом никогда не совпадают, и это конвенция, а не ошибка:
| что считаем | на август 2026 |
|---|---|
записей essay-item в индексе |
118 |
| сумма счётчиков секций | 114 |
файлов *.html в essays/ |
116 |
Расхождение сверху дают кросс-ссылки — записи, ведущие на эссе, которые живут внутри страниц объектов. Их четыре:
../objects/numbers/constants/pi.html#essay— §07../objects/numbers/constants/phi.html#essay— §07../objects/numbers/constants/euler.html#essay— §07../objects/statistics/inference/cuped.html#essay— §06
В индексе они выставлены, в счётчик секции не входят — счётчик считает
файлы в essays/. 118 − 4 = 114.
Заводя новую кросс-ссылку, счётчик секции и card-num не трогай:
запись прибавляется, число эссе — нет.
Расхождение снизу дают две редирект-заглушки, оставшиеся после
переименований (two-systems.html, cognitive-biases.html →
oshibki-po-pravilam.html). count_pages в scripts/update_counts.py
их отсекает — см. is_redirect. 116 − 2 = 114.
Инвариант, который должен держаться: сумма счётчиков секций равна числу
не-редиректных файлов в essays/, а в каждой секции счётчик в заголовке
равен числу в карточке категории. Оба проверяет scripts/preflight.py.
<title> — служебная строка: вкладка браузера, выдача поисковика
и og:title в превью ссылки. Она короткая и держит суффикс
— null/essays, из которого scripts/update_meta.py берёт тему.
h1 — заголовок страницы для читателя. Он может быть длиннее,
типографски полнее и иметь другую интонацию.
Расхождение между ними нормально и дефектом не считается. На август
2026 их 16 из 110: шесть отличаются только регистром и точкой, шесть —
h1 продолжает тему после запятой («xG» против «xG, или счёт врёт»),
четыре сформулированы по-разному («проблемы тысячелетия» против
«Семь задач. Семь миллионов долларов»).
Проверять надо другое, и это делает preflight 8: чтобы og:title,
canonical и og:url совпадали с тем, что записал бы update_meta.py.
Эти три сейчас сходятся на всех 110 страницах.
Заводя новый скрипт, дописывай сюда строку. Колонка «якорь» — то, за что скрипт цепляется в файле: если якорь уедет, скрипт либо промолчит, либо запишет не туда. Колонка «надзор» отвечает на вопрос, который иначе приходится задавать каждый раз заново.
| скрипт | пишет | якорь | надзор |
|---|---|---|---|
scripts/update_counts.py |
man.html, index.html — цифры |
16 regex по контексту (граф связей · , card-count-num, …) |
preflight 3 |
scripts/update_meta.py |
essays/*.html — мета-блок |
<!-- meta: … --> … <!-- /meta -->, вставка после <title> |
preflight 8 |
scripts/generate_sitemap.py |
sitemap.xml целиком |
якоря нет, файл пересобирается | preflight 2 |
data/build-backlinks.js |
страницы узлов — блок backlinks |
<section class="related">…</section> |
preflight 4 и 6 |
scripts/related-edges.py --apply |
data/links.json — рёбра |
<section class="related"> + <a …>…→</a> |
preflight 5 |
scripts/prose-edges.py --apply |
data/links.json — рёбра |
тело страницы за вычетом related, backlinks, series, xref, offsite |
preflight 9 |
Правила отбора надзор импортирует из самих скриптов — второй
реализации быть не должно, иначе разойдутся именно они. У этого правила
есть одно намеренное исключение: проверка 10 в preflight.py
дублирует разметку признаков секции related нарочно. Надзор,
собранный из тех же деталей, не увидит поломку детали: уедет якорь
<section class="related"> — related-edges.py молча пропустит
страницу, и проверка 5, импортирующая его же scan(), ослепнет ровно
там же и отрапортует «0 ✓». Проверка 10 смотрит независимо: ищет
related-links, related-link и подпись «связанное», а потом сверяет,
распознаётся ли на этих страницах канонический якорь.
Читают и ничего не пишут: scripts/preflight.py, graph-health.py,
оба извлекателя без --apply и шестнадцать скриптов в audit/,
которые кладут CSV рядом с собой.
1. правки + update_meta.py + update_counts.py
2. счётчики в essays/index.html руками (два числа на секцию)
3. git add + commit
4. generate_sitemap.py
5. git commit --amend
Пункты 3–5 именно в таком порядке: generate_sitemap.py берёт lastmod
из git, и для незакоммиченной страницы даты нет — запись попадает в карту
без <lastmod>. Один раз так и уехало на прод. Теперь скрипт в такой
ситуации ничего не пишет и завершается с ненулевым кодом.
Правило шире, чем «при добавлении страницы»: карту надо перегенерировать
после любого коммита, меняющего *.html. <lastmod> берётся из даты
последнего коммита, тронувшего файл, и меняется от любой правки — включая
машинную. Прогон data/build-backlinks.js не добавил ни одной страницы,
но сменил дату 57 файлам, и карта ушла отдельным коммитом уже после пуша.
Отставание карты от коммитов проверяет scripts/preflight.py.
Формулировка «ручное и машинное — разными коммитами» описывает результат, но не способ его получить. Способ один: закоммитить ручную правку до запуска генераторов.
Причина в том, что файл для git неделим. Если сначала поправить страницу
руками, а потом прогнать data/build-backlinks.js, обе правки окажутся
в одном файле, и развести их постфактум нельзя — только откатывать файл
и делать заново. Коммит ручного закрывает его от машинного.
Отсюда порядок при правке графа:
1. рёбра в data/links.json + ручные правки страниц
2. git commit ← закрывает ручное
3. node data/build-backlinks.js
4. python3 scripts/update_counts.py
5. git commit ← только машинный вывод
6. python3 scripts/generate_sitemap.py
7. git commit --amend
Это та же причина, по которой generate_sitemap.py идёт после коммита:
он читает состояние git, а не рабочего дерева. Разница лишь в том, что
там защита от неверных данных, а здесь — от неразделимого диффа.
Перед пушем — python3 scripts/preflight.py.
Файл подтверждения владения ресурсом https://yzotop.github.io/null/
в Google Search Console. Лежит в корне репозитория, отдаётся по
https://yzotop.github.io/null/google9b84a06f40ea405c.html.
Google перепроверяет подтверждение периодически, а не однократно при добавлении ресурса. Пропадёт файл — слетит подтверждение, а вместе с ним данные поиска и возможность отправлять карту сайта.
- содержимое не менять ни на байт, включая завершающий перевод
строки: его в файле нет, размер ровно 53 байта.
Контроль:
shasum -a 256даётddcd5ce0ab04a37986ed1edaaa1b7ce1ea764c806a52f85fdc1e21af264578c1 - не переименовывать и не переносить в подпапку: Google запрашивает точный путь
- в
sitemap.xmlне добавлять. Само не попадёт: файл внесён вEXCLUDEDвscripts/generate_sitemap.py. Без этой строки попадёт — генератор обходит все*.htmlна диске - токен один на аккаунт Google, а не на ресурс, поэтому файл тот же,
что в репозитории
davydov.my. Это нормально и так задумано
Проверка целостности:
echo "ddcd5ce0ab04a37986ed1edaaa1b7ce1ea764c806a52f85fdc1e21af264578c1 google9b84a06f40ea405c.html" | shasum -a 256 -c -В davydov.my эта проверка стоит в CI и валит сборку при порче
или пропаже. Здесь CI нет — руками, см. пункт в BACKLOG.md
того репозитория.
CSS-переменные определены в :root в styles/main.css.
Шрифты подгружаются с Google Fonts: Inter, JetBrains Mono, IM Fell English.
cd null
python3 -m http.server 8765Затем открыть http://localhost:8765/
Сборки нет, пакетов нет, node_modules нет — страницы отдаются как есть.
Внешнего кода три источника, все по CDN и без версии в репозитории:
- Google Fonts — Inter, JetBrains Mono, IM Fell English
- Chart.js 4.4.1 с
cdnjs.cloudflare.com— 7 страниц, и ещё одна тянет его же сcdn.jsdelivr.net - d3 7.8.5 с
cdnjs.cloudflare.com— толькоmap.html
JS на сайте есть: собственный <script> стоит на 64 страницах из 351 —
интерактивные визуализации в visuals/ живут на canvas.
Отдельно от страниц лежит data/build-backlinks.js — запускается руками
через node, генерирует блоки «упоминается в» из data/links.json.
Узел без входящих рёбер он пропускает целиком, поэтому выписанный руками
блок у такого узла остаётся, но графом не подтверждён и на карте не виден.
Вставить блок ему тоже некуда, если на странице нет <section class="related">
— он молча пропустит её при живых входящих рёбрах. Оба случая проверяет
scripts/preflight.py.
Единственная страница-узел с самодельной оболочкой. У неё собственные
<nav> и хлебные крошки с инлайн-стилями и свой футер внутри
<div class="page">; отсутствуют <header class="topbar">,
<div class="breadcrumb">, <main class="container">, <article>
и <footer class="footer">. Общий styles/main.css подключён, секции
related и backlinks — обычные.
Любая проверка, опирающаяся на эти классы, её не видит. За два захода
это сработало дважды: сначала не нашлось </article>, чтобы вставить
секцию, потом grep class="related" вернул ноль там, где блок был —
рукописный, без класса, и в итоге на странице оказалось два блока
«связанное» подряд.
Приводить к общей вёрстке не нужно: у страницы своя ширина и свой визуальный язык, перевод изменит её вид. Но при написании новой проверки по общим классам эту страницу надо держать в уме.
Без <header class="topbar"> в репозитории ещё шесть файлов, и все
они законны: корневой index.html со своей вёрсткой, две
редирект-заглушки в essays/, два индекс-редиректа
(charts/index.html, objects/books/index.html) и прототип
в tools/ под .gitignore. Страница-узел с чужой оболочкой — одна.
related пишется руками, backlinks собирает генератор. Поэтому подпись
одной и той же ссылки в двух местах может не совпадать, и это не
рассогласование:
| блок | кто пишет | подпись |
|---|---|---|
related — «связанное» |
автор | проза, под контекст страницы |
backlinks — «упоминается в» |
build-backlinks.js |
title узла буквально |
Пример: в objects/notation/alphabets/hebrew.html стоит «ZFC —
независимость аксиом», тогда как title того же узла в графе —
«Математика которую нельзя доказать». Копировать title в related
ради единообразия не надо. Метка типа (related-link-type) в related
тоже авторская и грубее машинной: широкое «объект» стоит в 275 местах
там, где генератор написал бы «константа» или «теорема».
Отношение между секцией и графом одностороннее:
- ссылка в
relatedбез ребра — дефект. Страница утверждает связь, которой нет на карте и не будет в чужих блоках «упоминается в». - ребро без ссылки в
related— норма. Секция курируется автором и показывает не всё: у активных узлов исходящих бывает под два десятка, в секции — пять-восемь.
То есть множество ссылок в related должно быть подмножеством
исходящих рёбер узла, а не совпадать с ними. Автоматической проверки
на это нет: backlinks держит генератор, а related — только память.
На август 2026 нарушений первого рода 287 на 161 странице из 311.