Бесплатный интерактивный курс · aicoding.space
package-story-page: страница пакета, где доказано каждое число
Практический курс по навыку @dzhechkov/skills-package-story-page для того, кто впервые собирает страницу-историю о чужом или своём пакете. Вместе с Ниной ты разберёшь порядок восьми блоков истории, форму доказательства из пути, хеша и узкого диапазона строк, три статуса утверждения, отдельную заставу для чисел, обязательный ярлык синтетического примера, конвейер из четырёх шагов, закрытое подмножество HTML у проверяющего, отрицательный контроль в браузерной заставе, честные границы проверок — и научишься выбирать между страницей-историей и полным курсом.
Содержание курса
1. Зачем нужна страница-история
Что делает package-story-page и чем он отличается от ещё одного README.
Ключевая мысль: страница-история для неспециалиста
Нина сопровождает небольшой пакет. Раз в неделю кто-нибудь из соседней команды спрашивает: «а он вообще что делает?» — и она в сотый раз пересказывает README вслух.
README написан для того, кто уже решил ставить пакет. Человеку со стороны нужно другое: страница-история — короткая страница, где сначала показан живой пример работы, и только потом устройство, установка и границы.
Ровно это собирает навык package-story-page: берёт один существующий пакет и делает из него самодостаточный HTML-файл — без внешних шрифтов, картинок, скриптов и сетевых запросов при показе.
- пакет на npm: @dzhechkov/skills-package-story-page;
- исходники и README: репозиторий пакета.
Отличие от обычного лендинга жёсткое: здесь нельзя написать «самый быстрый в мире». Каждое утверждение с фактом обязано указывать на конкретные строки в файлах пакета — иначе страница не пройдёт собственную проверку и не будет собрана.
Нина сначала расстроилась: «мне же нужно продать инструмент». Через день она поняла обратное — доказанная страница убеждает сильнее, потому что читатель может сам открыть указанный файл и увидеть ту же строку.
2. Пример раньше архитектуры
Восемь блоков истории и жёсткое правило их порядка.
Ключевая мысль: пример раньше архитектуры
Первая версия страницы у Нины начиналась так: «Архитектура решения основана на модульном подходе…». Читатель со стороны закрыл вкладку на второй строке.
Контракт истории (references/story-contract.md) задаёт восемь блоков и требует авторовать их именно в этом порядке:
hero— одна проблема аудитории, одна фраза о пользе, одно основное действие;example— конкретный вход, от двух до пяти шагов обработки, предпросмотр результата;why— зачем пакет существует и что меняется до и после;mechanism— от трёх до шести названных стадий с пояснением и ограничителем у каждой;install— точные команды, подтверждённые данными о пакете;reuse— где ещё это запускается; неизвестная поддержка так и остаётся неизвестной;limits— чего пакет не делает: безопасность, стоимость, свежесть;cta— следующий безопасный шаг.
Правило, которое навык называет непререкаемым: пример появляется раньше архитектуры, установки, стоимости, безопасности и раздела вопросов. Не «желательно раньше» — раньше.
Причина простая и проверяемая на себе: человек решает, читать ли дальше, по первому экрану. Если там абстракция, второго экрана не будет. Нина переставила блоки — и тот же коллега дочитал до конца.
3. Из чего состоит доказательство
Путь, SHA-256 и диапазон строк — форма локальной ссылки на источник.
Ключевая мысль: узкий диапазон строк с SHA-256
Нина написала первое утверждение и сослалась на весь README целиком: «там же это написано».
Контракт доказательств (references/evidence-contract.md) такую ссылку не принимает. Источник бывает ровно двух видов:
- локальный:
{ id, path, sha256, lineRange }— относительный путь, хеш файла и диапазон строк; - внешний:
{ id, url, checkedAt, receiptPath, sha256, lineRange }— адрес по HTTPS, дата проверки и локальная квитанция, которая сама называет тот же адрес и ту же дату.
Диапазон строк ограничен сверху сорока строками (MAX_LOCAL_RANGE_LINES в scripts/story-schema.mjs). Это и есть лечение от ссылки «весь README»: нельзя процитировать файл целиком и назвать это доказательством — прочитай файл и сузь диапазон до тех строк, на которых утверждение действительно держится.
Проверяющий не верит брифу на слово: он заново читает файл под --pkg, сверяет путь и хеш с извлечёнными данными, отказывается идти по символическим ссылкам и наружу из каталога пакета.
И сразу честная оговорка, которую контракт делает сам: указатель на источник доказывает, что пакет ЭТО СКАЗАЛ, а не что сказанное истинно. Документация пакета может врать; SHA-256 покажет, какие именно байты сделали заявление. Нина держит эту фразу в голове всё время — она отделяет провенанс от истины.
4. Три статуса утверждения
evidenced, external, unknown — и почему пустой список источников допустим только у одного из них.
Ключевая мысль: статус утверждения evidenced, external или unknown
У Нины на столе три фразы, и все три хочется поставить на страницу.
Каждое утверждение в брифе выглядит так: { id, text, status, sourceIds }. Статус — ровно один из трёх:
- evidenced — подтверждено текущими локальными строками пакета;
- external — опирается на внешнюю запись с датой и локальной квитанцией;
- unknown — доказательства нет, и страница честно показывает это читателю.
Ловушка, на которую Нина попалась: пустой список источников разрешён ТОЛЬКО при статусе unknown. Написать красивую фразу, не сослаться никуда и оставить статус evidenced не выйдет — схема отклонит бриф с сообщением, что утверждение должно либо цитировать доказательство, либо быть явно неизвестным.
И вторая ловушка, поважнее: unknown — это не позор и не заглушка. Это единственный честный способ сказать «мы не знаем», и интерфейс показывает такой пункт с видимой пометкой отсутствующего доказательства. Страница, где всё подряд помечено evidenced, вызывает больше подозрений, чем страница с двумя честными unknown.
Ещё одно ограничение, которое стоит запомнить сразу: числовое утверждение обязано быть evidenced. Внешняя запись с числом детерминированную проверку не проходит — либо приносите число как текущее локальное доказательство, либо помечайте unknown.
5. Числа проходят через отдельную заставу
Почему число в подзаголовке отклоняется, а то же число в утверждении — нет.
Ключевая мысль: числовой токен с контекстом на той же строке
Нина написала подзаголовок: «74 живых мутанта охраняют этот верификатор». Красиво, правда — и проверка отклонила бриф целиком.
Сюрприз в том, что дело было не в самом числе, а в том, ГДЕ оно стоит. Схема делит текст на две категории:
- утверждения (
claims) — туда числа можно; - прочая авторская проза — заголовки, подписи, пояснения стадий, направления визуалов: числовой токен там запрещён вовсе.
Правило звучит так: фактические числа выражай утверждением, а не украшением. Исключение сделано ровно для отображаемых артефактов — синтетического ввода, предпросмотра результата и буквальных команд установки: там цифры это часть показываемого объекта, а не обещание.
А внутри утверждения число охраняется отдельно. Каждому числовому токену нужна своя запись доказательства с тремя полями:
token— точно тот же токен, и записи идут в том же порядке, что и числа в тексте;context— поле или единица измерения рядом, длиной от трёх до восьмидесяти знаков, обязательно с буквами и без цифр (контекст не может быть самим числом);sourceId— источник, который уже назван вsourceIdsэтого утверждения.
И главное: токен и его контекст должны встретиться на ОДНОЙ строке внутри процитированного диапазона. Не в одном файле, не по соседству — на одной строке. Так число «74» получает смысл «74 чего именно», и его нельзя подставить из случайного места.
Нина переписала фразу как утверждение со ссылкой на реестр мутаций — и застава пропустила её без единого возражения. Кстати, десятичные цифры распознаются в любой системе письма, а слова «семьдесят четыре» токеном не считаются: это уже вопрос содержательного обзора, а не грамматики.
6. Синтетический пример и его видимый ярлык
Почему демонстрация обязана называть себя демонстрацией.
Ключевая мысль: синтетический пример с обязательным видимым ярлыком
Нина сделала красивый пример: вход, три шага, готовый результат. Всё придумано ею за пять минут — и выглядит как настоящий рабочий прогон.
Именно поэтому навык требует у каждого примера поле synthetic: true, и это не служебный флаг: он превращается в видимый ярлык на странице. Читатель узнаёт, что перед ним показательный пример, а не запись реальной работы у реального клиента.
Отказаться от ярлыка нельзя — схема требует synthetic: true буквально, любое другое значение отклоняет весь пример.
Второе требование того же уровня: показывай проверяемый результат, а не фразу «дальше происходит магия». Предпросмотр результата и его формат — обязательные поля примера; синтетический пример без результата схема не принимает.
Что запрещено прямо и без оговорок: выдуманные отзывы, клиенты, метрики, награды и искусственная срочность. Популярность, цены, результаты тестов производительности и совместимость нельзя придумывать — только приносить доказательства.
Нина проверила себя одним вопросом: «если читатель откроет исходники, он почувствует себя обманутым?» Ярлык synthetic — это способ ответить «нет» заранее, а не оправдываться потом.
7. Конвейер: извлечь, написать, отрисовать, проверить
Четыре обязательных шага и пятый — перед заявлением о выпуске.
Ключевая мысль: конвейер из четырёх шагов и браузерная застава
Нина хочет один раз увидеть весь путь целиком, а не собирать его из обрывков.
Конвейер навыка состоит из четырёх шагов, и каждый — обычная команда node:
- извлечь доказательства:
extract-package-evidence.mjs --pkg <корень пакета> --json evidence.json— обходит файлы пакета, считает хеши, собирает примеры из README и честно перечисляет то, что доказать не смог; - написать бриф — единственный шаг, который делает человек или модель: скопировать в бриф идентификатор, путь и хеш каждого источника из шага 1 и сузить диапазон строк;
- отрисовать страницу:
render-story-page.mjs --brief brief.json --out site/index.html; - проверить:
verify-story-page.mjs --brief … --site … --evidence … --pkg …— при любом красном результате конвейер останавливается.
Пятый шаг отделён намеренно: verify-story-page-browser.mjs --site … запускает настоящий Firefox и измеряет вёрстку. Он требуется перед заявлением о выпуске — и его нельзя заменить ни свойством overflow-x: hidden, ни утверждением о тексте стилей.
Обрати внимание на форму команд: всё запускается как node "$SKILL_ROOT/scripts/<файл>.mjs". SKILL_ROOT — абсолютный путь к каталогу, в котором лежит установленный SKILL.md. Искать соседние файлы в родительских каталогах репозитория навыку запрещено: он должен работать одинаково и внутри монорепозитория, и в одиночной установке.
Нина попробовала пропустить шаг 4 — «и так же видно, что страница нормальная». На следующей правке брифа выяснилось, что один идентификатор источника потерялся, и страница ссылалась в пустоту. Проверка ловит это за секунды; глаз не поймал за день.
8. Закрытое подмножество HTML
Что именно проверяет разбор страницы — и чем он принципиально не является.
Ключевая мысль: закрытое подмножество HTML вместо сканера чужих страниц
Нина спросила прямо: «Ваш проверяющий — это сканер безопасности? Можно натравить его на чужой сайт?»
Ответ: нет, и это записано в README пакета отдельным абзацем. Проверяющий предназначен для страниц, которые собрал ЭТОТ ЖЕ конвейер. Он разбирает намеренно закрытое подмножество HTML привезённой с собой сборкой parse5, не исполняет ни строчки скриптов страницы и не делает сетевых запросов.
Что он отклоняет внутри своего подмножества:
- ошибки разбора и любые починки, которые браузер сделал бы молча;
- синтезированные и не имеющие позиции в исходнике узлы;
- чужие пространства имён и комментарии;
- всякий элемент и атрибут за пределами выпускаемого набора.
Отдельная деталь про стили: единственный элемент <style> принимается только тогда, когда SHA-256 его точных байтов совпадает с отпечатком, который хранит сам проверяющий. Ссылки на источники обязаны быть объявленными в брифе абсолютными адресами по HTTPS с атрибутом rel="noreferrer" и находиться внутри раскрывающегося блока источников.
Перед тем как выполнить хоть один локальный модуль, обёртка читает и закрепляет по SHA все пять загружаемых локальных входов: семантический проверяющий, сборку parse5, извлечение доказательств, отрисовку и общую схему. Отдельная застава графа импортов спрашивает у разборщика Node, какие модули запрашиваются, и пропускает только относительные рёбра .mjs внутри каталога плюс ровно node:crypto.
Если изолированный разборщик не удалось запустить или он вернул пустой, несовместимый или нетипизированный результат — застава закрывается с названной причиной, а не пропускает молча. Это и есть разница между «проверка прошла» и «проверка не смогла сказать»; смешивать их нельзя. И честная граница напоследок: ни один из этих механизмов не обещает удержать произвольный или намеренно заново разрешённый код — окончательной властью остаются проверенные побайтовые хеши.
9. Ноль запросов — ещё не доказательство
Отрицательный контроль: прибор сначала обязан доказать, что он вообще видит.
Ключевая мысль: отрицательный контроль перед выводом о нуле запросов
Нина запустила браузерную заставу и получила строку: сетевых запросов на другое происхождение — ноль. Победа?
Вопрос, который стоит задать первым: а прибор вообще способен увидеть такой запрос? Ноль бывает двух совершенно разных видов — «страница ничего не запросила» и «измеритель сломан и молчит». Отличить их по самому нулю невозможно.
Поэтому застава устроена так: перед измерением она поднимает вторую точку и заставляет страницу-приманку сходить туда. Записывающий посредник обязан этот запрос увидеть и отклонить. Только после этого ноль запросов с настоящей страницы засчитывается как доказательство.
Такой шаг называется отрицательный контроль — намеренно вызванное событие, которое прибор должен зарегистрировать, чтобы его молчание в остальное время что-то значило.
Дальше идёт ещё один слой, и он про подлог со стороны вызывающего кода:
- измерения принимают только непрозрачную квитанцию, выданную самим модулем наблюдения; структурно похожий объект её не подделывает;
- посредник сам читает адрес контрольной цели и живой счётчик попаданий, поэтому место вызова не может подставить готовую пару «да, ноль».
Нина сформулировала для себя правило шире, чем этот пакет: отсутствие квитанции — не успех. Если проверка выводит «прошло» из тишины, она тихо ломается на каждом новом виде отказа — и никто об этом не узнает.
10. Установка и запуск из SKILL_ROOT
Как поставить пакет, проверить подпись и запустить первый шаг.
Ключевая мысль: запуск скриптов только из SKILL_ROOT
Хватит теории — Нина уже открыла терминал, открывай и ты.
Пакет опубликован: @dzhechkov/skills-package-story-page на npmjs.com, исходники — в репозитории. Путь до первого запуска:
- поставь пакет навыков в проект:
npm i -D @dzhechkov/skills-package-story-page; - если тебе нужна подтверждённая подлинность, до любого запуска выполни
dz verify-packна распакованном артефакте — README называет это предварительным условием со стороны вызывающего, и одна лишь установка навыков его не заменяет; - назначь корень навыка:
SKILL_ROOT=./node_modules/@dzhechkov/skills-package-story-page/package-story-page; - сделай первый шаг конвейера — извлечение доказательств.
Почему SKILL_ROOT, а не относительные пути. SKILL_ROOT — абсолютный каталог, в котором лежит установленный SKILL.md. Навыку прямо запрещено искать свои вспомогательные файлы в родительских каталогах монорепозитория: иначе он работал бы у автора и ломался у любого, кто поставил пакет обычным способом.
Грабли, на которые наступила Нина: она запустила скрипт из каталога репозитория, где рядом лежал знакомый ей путь, и получила совсем другую сборку. Правило простое — каждый запуск идёт через node "$SKILL_ROOT/scripts/<файл>.mjs".
Отдельно про подпись: прямая проверка dz verify-pack в авторском каталоге по замыслу падает на package.json, потому что при публикации из него удаляется хук жизненного цикла prepublishOnly. Проверять нужно распакованный опубликованный артефакт — именно он является подписанным объектом для потребителя.
11. Честные границы: что страница НЕ доказывает
Раздел, который читают последним и жалеют, что не первым.
Ключевая мысль: честные границы того, что проверка не доказывает
В README пакета есть раздел «Honest scope», и Нина советует читать его раньше всего остального.
Что проверки доказывают машинно: текущий SHA файла и ограниченный диапазон строк, точный числовой токен с контекстом на одной строке, явную пометку синтетического примера, совпадение имени пакета, точное расположение авторских полей, замкнутость идентификаторов источников, владение доказательством и статусом для каждого пункта — без скрытых, осиротевших и лишних владельцев.
Что они не доказывают — и это записано там же:
- что утверждение истинно за пределами пакета;
- что страница красива;
- что она приводит к целевому действию;
- что она полностью соответствует стандарту доступности.
Всё это остаётся содержательным и визуальным обзором, то есть работой человека.
Самая важная фраза раздела звучит так: документация самого пакета может содержать ложные утверждения; хеш доказывает, какие байты сделали заявление, а не то, что заявление независимо истинно.
Границы есть и у ресурсов: файлы доказательств читаются как строгий UTF-8 через дескрипторы без переходов по символическим ссылкам, с потолком в один мебибайт на файл и восемь мебибайт суммарно; графы объектов глубже шестидесяти четырёх уровней отклоняются до разбора схемы. Это границы ресурсов и декодирования — не гарантии истины.
И бытовая, но важная деталь: браузерная застава требует системных бинарников браузера. Команда npm test запускает её по умолчанию, а PACKAGE_STORY_SKIP_BROWSER=1 npm test — это явно ослабленная полоса только с модульными тестами, и называть её полной проверкой нельзя.
12. Выбор между страницей и курсом
Твоя очередь решать: story-page или tutorial-factory.
Ключевая мысль: выбор между страницей-историей и полным курсом
Последняя секция — твоя. Нина уже сделала свой выбор, теперь очередь за тобой.
У навыка есть родственник: package-tutorial-factory. Оба берут на вход один существующий пакет, и разница между ними — не в качестве, а в жанре:
- страница-история нужна, когда человеку надо ПОКАЗАТЬ: один живой пример, устройство, установка, границы. Читают за пять минут, экзамена нет;
- полный курс нужен, когда человека надо НАУЧИТЬ: секции по порядку, упражнения, достижения, финальный тест.
Этот выбор не оставлен на удачу. В пакете лежит оценка маршрутизации: файл evals/routing.yaml с положительными и отрицательными примерами запросов на двух языках, запускаемая как evals/run-pair-routing.mjs с параметром --out. Отдельная деталь честности эксперимента: ожидаемые владельцы запроса не попадают в приглашение судьи — иначе судья читал бы ответ прямо в вопросе.
Устанавливаемая отдельно копия навыка может передать соседний SKILL.md параметром --sibling-skill, чтобы сравнение шло с настоящим текстом соседа, а не с представлением о нём.
Твоя задача в упражнении ниже: три запроса, и по каждому надо решить, чей это случай. Готового правила «если в запросе есть слово „курс“» не существует — читай, чего человек хочет на выходе: показать или научить.
Частые вопросы
- Можно ли натравить проверяющий на чужую страницу, чтобы найти в ней дыры?
Нет. Он рассчитан на выход собственного конвейера: разбирает намеренно закрытое подмножество HTML, не исполняет скрипты страницы и не является сканером безопасности для произвольного стороннего HTML. Для чужих страниц нужен другой инструмент.
- У меня нет Firefox и geckodriver. Что я теряю?
Живую проверку вёрстки на четырёх ширинах, измерение фокуса и контраста и наблюдение за сетевыми запросами с отрицательным контролем. Запуск npm test с переменной PACKAGE_STORY_SKIP_BROWSER даёт только модульную полосу — README называет её явно ослабленной, и выдавать её за полный прогон нельзя.
- Я хочу написать «пакет ставят тысячи команд». Как это провести через проверку?
Никак, если доказательства нет. Числовое утверждение обязано иметь статус evidenced с локальными строками; внешняя запись с числом детерминированную проверку не проходит. Честный выход — убрать фразу или заменить её тем, что действительно доказано.
- Почему dz verify-pack падает прямо в каталоге пакета?
Так и задумано. При публикации из package.json удаляется хук жизненного цикла prepublishOnly, поэтому авторский каталог и опубликованный артефакт отличаются ровно на эту строку. Подписанным объектом для потребителя является распакованный опубликованный артефакт — проверять надо его.
- Чем страница-история отличается от соседнего навыка package-tutorial-factory?
Жанром результата. Страница-история показывает работу пакета: один живой пример, механизм, установка, границы, пять минут чтения. Полный курс учит пользоваться: секции по порядку, упражнения, достижения и финальный тест. Выбор проверяется запускаемой оценкой маршрутизации внутри самого пакета.
- Что делать, если извлечение доказательств не смогло что-то прочитать?
Ничего не скрывать. Извлечение само перечисляет непрочитанное в списке unknowns и отдельно сообщает, сколько источников и примеров оно включило из найденных — ограниченный обход никогда не выдаётся за полный. Оставь такие пункты со статусом unknown, и страница честно покажет отсутствие доказательства.
- Обязательно ли начинать страницу с примера, если пакет сложный?
Да, это правило контракта, а не стилистический совет: пример появляется раньше архитектуры, установки, стоимости, безопасности и раздела вопросов. Если пакет сложный, сузь пример до одного входа и одного проверяемого результата — но не меняй порядок.