diff --git a/apps/website/i18n/queen-and-hive.ru.json b/apps/website/i18n/queen-and-hive.ru.json new file mode 100644 index 0000000000..5e3268a858 --- /dev/null +++ b/apps/website/i18n/queen-and-hive.ru.json @@ -0,0 +1,242 @@ +{ + "$spec": "specs/course/queen-and-hive-ru.t27", + "$note": "Russian words for specs/course/queen-and-hive.t27. Keys are MODULE_IDS, LESSON_IDS and, under ui, the SAY_ names. Numbers and {placeholders} must match the English field; course-from-spec.mjs checks both.", + "locale": "ru", + "ui": { + "TITLE": "Королева и улей: как проект t27 строит себя сам", + "DESCRIPTION": "Курс 11, 27 уроков, в каждом живой виджет и спека t27: оформление задачи, приоритет и сначала ревью, жизнь задачи, ревьюер и ворота слияния, затор ревью, ключи, заслуги и токены, агенты в одной группе, ночной дозор и доска. Каждый ответ даёт скомпилированная спека.", + "KICKER": "Курс", + "LEAD": "Этот проект строит себя сам: задача становится работой для пчелы, пчела пишет спеку t27, пчела-ревьюер её проверяет, спеку сливают, и сайт пересобирается. Королева, которая ведёт этот цикл, записана спеками t27 с тестами, и каждый виджет здесь исполняет тела fn одной из них, скомпилированные в WebAssembly.", + "SHAPE": "9 модулей по 3 урока, 27 клеток. Заполненная клетка — урок, отмеченный пройденным.", + "START": "Начать урок 1", + "CONTINUE": "Продолжить", + "MODULE": "Модуль {0}", + "LESSON": "Урок {0} из {1}", + "GOAL": "Вы узнаете", + "TRY": "Попробовать", + "ALSO": "Также откройте", + "ALSO_HINT": "Каждая кнопка меняет виджет сверху.", + "BACK_TO_MAIN": "Вернуться к виджету урока", + "SPEC": "Открыть спеку урока в плеере", + "OPEN_PAGE": "Открыть виджет на отдельной странице", + "WIDGET_LANG": "Виджеты хранят свои английские слова: у каждого своя спека t27.", + "PREV": "Назад", + "NEXT": "Далее", + "ALL": "Все уроки", + "MARK": "Отметить пройденным", + "MARKED": "Пройдено", + "PROGRESS": "{0} из {1} пройдено", + "PRIVATE": "Прогресс хранится только в этом браузере; ничего не отправляется.", + "SOURCE": "Этот курс сам является спекой t27: прочитайте её", + "COHORT": "Ищете платное обучение по FPGA? Оно переехало на отдельную страницу.", + "NOT_FOUND": "У урока нет такого адреса.", + "OPEN_LESSON": "Открыть интерактивный урок", + "OPEN_COURSE": "Открыть интерактивный курс", + "SEO_TITLE": "{0}: курс «Королева и улей», урок {1} из {2}", + "SHARE": "Ссылка для шаринга, с карточкой предпросмотра", + "NEXT_COURSE": "Следующий курс", + "PREV_COURSE": "Предыдущий курс" + }, + "modules": { + "the-task": { + "TITLE": "Задача", + "LINE": "Задача становится работой, только когда её оформили: недостающие разделы дописаны или запрошены, граница называет настоящие файлы, а один фильтр читает задачи всех видов." + }, + "who-goes-next": { + "TITLE": "Кто следующий", + "LINE": "Порядок, в котором Королева берёт задачи: метка стареет на одну ступень, критичными идут не больше 3, и ничего нового не начинается, пока готовая работа ждёт ревью." + }, + "a-bees-life": { + "TITLE": "Жизнь пчелы", + "LINE": "От кандидата до готовой работы: двенадцать состояний и шестнадцать событий, корабль в игре, который улетает домой, и деплой, который не обрывает пчелу." + }, + "the-reviewer": { + "TITLE": "Пчела-ревьюер", + "LINE": "Готовую работу судят по своим часам: столько ревью сразу, чтобы успевать, пауза, которая удваивается для строки в ожидании, и ворота слияния, которые не открыть без проверок." + }, + "the-jam": { + "TITLE": "Затор ревью", + "LINE": "Застрявший пулл-реквест всегда в одном из десяти случаев, у каждого свой ход Королевы, а красная проверка считается унаследованной, только если так говорит master." + }, + "keys": { + "TITLE": "Ключи", + "LINE": "Ключ модели у пчелы могут отклонить: код в теле ответа сортирует отказ, временный отказ даёт ключу отдохнуть секунды, и пчела переходит к следующему свободному ключу." + }, + "credit-and-money": { + "TITLE": "Заслуги и деньги", + "LINE": "Одолженный ключ не приносит заслуг, операция продаётся вдвое дороже своей цены в токенах по полцента, а токены сгорают через 30 дней после зачисления." + }, + "agents-together": { + "TITLE": "Агенты между собой", + "LINE": "У каждого агента свой бот в одной группе: кого зовут, цепочка глубиной в три сообщения ботов и часы, которые не дают агентам завалить чат." + }, + "night-and-the-board": { + "TITLE": "Ночь и доска", + "LINE": "Что показывает улей: ночной дозор, который открывает, комментирует и закрывает задачи, колонки публичной доски и соты, которые вспыхивают на каждое событие." + } + }, + "lessons": { + "shaping-an-issue": { + "TITLE": "Оформление задачи", + "GOAL": "Что Королева делает с задачей без нужных разделов и почему останавливается после двух попыток.", + "TEXT": "Пчела может начать только задачу, в которой сказано, что сделать и каких файлов можно касаться. Королева проверяет каждую открытую задачу функцией judge() из specs/policy/issue_shape.t27. Неготовую она оформляет: модель дописывает недостающие разделы, и текст возвращается в задачу, только если judge() больше ничего не находит. После SHAPE_ATTEMPT_LIMIT, 2 неудачных попыток, она останавливается и один раз пишет комментарий, чего не хватает, а эпик не оформляется никогда. Решает shape_action в shaping.t27, а новая попытка ждёт 360 минут.", + "TASK": "Поставьте два недостающих раздела и пройдите попытки; затем отметьте, что автору уже написали, и посмотрите, в какую колонку попадёт задача." + }, + "a-boundary-of-real-files": { + "TITLE": "Граница из настоящих файлов", + "GOAL": "Какие предложенные пути задача может забронировать и почему один выдуманный путь останавливает всё оформление.", + "TEXT": "Граница задачи — это бронь: пока пчела работает, другие пчёлы эти файлы не берут. Поэтому неверная граница хуже отсутствующей: она держит файлы, которые работе не принадлежат. path_accepted принимает любой существующий файл, а новый — только если существует его папка, задача его упоминает и язык разрешён правилом only-t27. shape_lands возвращает оформление в задачу, только если все разделы на месте, путей от 1 до 12 и ни один не отклонён.", + "TASK": "Сделайте два красных пути зелёными, меняя только то, что может быть правдой; затем оставьте раздел недописанным и посмотрите на ворота." + }, + "one-task-three-kinds": { + "TITLE": "Одна задача, три вида", + "GOAL": "Как задача, ревью и фоновая работа становятся одним видом задачи с семью состояниями и как их читает один фильтр.", + "TEXT": "Королева ведёт работу трёх видов: задачи для пчёл, ревью готовой работы и фоновые работы. У каждого вида были свои коды, и ни одна доска не могла показать их вместе. tasks.t27 даёт им 7 общих состояний, пронумерованных как колонки доски, плюс failed. Отменённая работа показывается как dropped, и так же — забытая строка ревью. task_matches — фильтр, который применяют доска, игра и маршрут: бит на вид, бит на состояние, а 0 значит любое.", + "TASK": "Покажите только упавшие задачи всех видов; затем найдите, какое состояние работы показывается как dropped." + }, + "aging-a-label": { + "TITLE": "Старение метки", + "GOAL": "Как задача с меткой поднимается с возрастом и почему только на один уровень.", + "TEXT": "Без меток Королева берёт задачи в том порядке, в котором их выдаёт GitHub. priority.t27 добавляет четыре уровня: critical, high, normal и low; меньший идёт раньше, а задача без метки считается normal. Задача с меткой поднимается на уровень через AGING_DAYS, 14 дней, и не больше чем на MAX_AGING_STEPS, 1 уровень, так что low доходит до normal на 14-й день и там остаётся. High и critical не стареют. Линию в виджете вычисляет effective_level.", + "TASK": "Найдите день, когда задача с меткой low поднимается; затем снимите метку и дотяните до 60-го дня." + }, + "the-queue": { + "TITLE": "Очередь", + "GOAL": "Как потолок, старение и блокеры упорядочивают очередь открытых задач.", + "TEXT": "Метки раздуваются: если каждая задача критична, критичных нет. capped_level позволяет идти как critical не больше чем CRITICAL_CAP, 3, задачам списка, в порядке списка, а 4-я идёт как high. Задача с открытым блокером пропускается, её не ждут, и она не может задержать очередь. outranks сравнивает уровень с учётом старения, затем настоящий уровень, затем место в списке, так что постаревшая задача никогда не обгонит настоящую своего уровня. why называет причину каждого места.", + "TASK": "Сделайте критичной пятую задачу и посмотрите, куда она встанет; затем поставьте задаче без метки метку low." + }, + "review-first": { + "TITLE": "Сначала ревью", + "GOAL": "Почему Королева закрывает бэклог, пока готовая работа ждёт ревью, и что всё равно идёт.", + "TEXT": "На 2026-10-18 страница статуса показывала 69 из 70 занятых воркеров, а за час — 102 запуска, 56 завершённых и 11 проверенных. Пчёл запускалось примерно девять на каждый вердикт, и каждая новая пчела держала файлы, нужные пчёлам на ревью, когда их возвращали. review_first.t27 закрывает бэклог, пока очередь ревью длиннее REVIEW_QUEUE_MAX, 0. Работа, которую ревью вернуло, и работа, назначенная владельцем, всё равно идут: admission пропускает их при любой длине очереди.", + "TASK": "Перетащите очередь на 0 и обратно на 1; затем найдите кандидатов, которых пропускают при любой длине." + }, + "a-tasks-life": { + "TITLE": "Жизнь задачи", + "GOAL": "Через какие состояния проходит запущенная задача и какие события допустимы в каждом.", + "TEXT": "dispatch.t27 — жизненный цикл задачи у Королевы, цикл, который выбирает, запускает, завершает, проверяет и отпускает работу. У задачи 12 состояний и 16 событий. next_state отвечает на каждую пару, а 99 значит, что событие в этом состоянии недопустимо: вердикт не доходит до работающей задачи, а снятие — до завершённой. После вердикта возврат ждёт свой порог и отпускается; даже принятую работу возвращают, если CI краснеет ниже потолка.", + "TASK": "Проведите задачу от none до accept; затем сделайте CI красным на потолке и найдите выход, который остаётся." + }, + "the-ship-flies-home": { + "TITLE": "Корабль летит домой", + "GOAL": "Где игра рисует пчелу в каждом её занятии и как завершённая задача заканчивается на экране.", + "TEXT": "В игре на app.t27.ai/game каждая пчела — корабль над задачей, над которой она работает. ship_mode в hive.t27 выбирает место: стоянка у входа, пока раннер её не взял, облёт своей ячейки, пока от неё приходят вести, зависание, когда она замолчала, и ожидание у портала своего репозитория, пока ячейку не поставили. Когда пчела уходит из ленты, она летит домой, и ship_alpha гасит её за DEPART_MS, 2400 мс: через 1200 мс у неё 500 из 1000.", + "TASK": "Отметьте, что от пчелы давно нет вестей, и посмотрите, куда она сдвинется; затем протяните время дальше 2400 мс." + }, + "a-deploy-mid-flight": { + "TITLE": "Деплой на лету", + "GOAL": "Что передеплой делает с пчелой, ревью, раундом, фоновой работой и ожиданием.", + "TEXT": "Каждое слияние передеплоивает Королеву, а в этот момент идёт работа. drain.t27 её сортирует. Раунд, шаг фоновой работы и строка ожидания — это строки, которые хранит база: процесс их оставляет, и следующий продолжает. Ревью останавливается: остановка теряет не больше одного ревью, и его делают заново. Пчелу держат, потому что сделанное ею живёт в её разговоре; на потолке, drain_cap_seconds, сегодня 1800 с, её возвращают, а работу сохраняют в её ветку.", + "TASK": "Протяните ожидание от 0 до потолка и посмотрите, как меняется строка пчелы; затем сократите дренаж платформы до 1000 с." + }, + "enough-reviewers": { + "TITLE": "Хватит ли ревьюеров", + "GOAL": "Сколько ревью должно идти сразу, чтобы ревьюер успевал за пчёлами.", + "TEXT": "Ревью может занять REVIEW_ROW_SECONDS, 300 с, поэтому один слот проверяет не больше 12 строк в час. На 2026-10-09 пчёлы заканчивали около 58 в час, пока ждали 111 строк, а постоянный REVIEWER_CONCURRENCY, равный 4, даёт не больше 48. slots_needed(58) равно 5. reviewer_concurrency учитывает ключи, их полосы и свободную память, никогда не поднимается выше 16 и не опускается ниже 1, пока что-то ждёт, а keeps_up сравнивает это с темпом завершения.", + "TASK": "Найдите наименьшее число ключей с 1 полосой, при котором ревьюер успевает за 58 в час; затем урежьте память до 1000 МБ." + }, + "back-off": { + "TITLE": "Отступить", + "GOAL": "Почему строку, которая отвечает wait, проверяют всё реже и как часто хотя бы.", + "TEXT": "За 44 минуты на 2026-10-09 ревьюер сделал 941 визит к строкам, из них 888 с ответом wait, от 22 строк, чью ветку он не мог прочитать; одну строку проверили 54 раза. Теперь revisit_after_seconds ждёт 30 с после одного wait и удваивает паузу на каждый следующий, не больше 900 с: 30, 60, 120, 240, 480, 900. due_again даёт взять строку, только когда это время прошло, а любой другой ответ сбрасывает счёт.", + "TASK": "Найдите число ожиданий, после которого пауза перестаёт расти; затем поставьте 4 ожидания и найдите секунду, когда строка снова к сроку." + }, + "the-merge-gate": { + "TITLE": "Ворота слияния", + "GOAL": "Когда мерджер может слить пулл-реквест, который одобрила пчела-ревьюер.", + "TEXT": "Пчела-ревьюер одобряет, мерджер сливает; merger_gate.t27 — правило между ними. check_passes пропускает зелёную проверку и задерживает любую, которая ещё идёт. Красная проходит, только если она не обязательная, завершилась и пчела назвала её списанной, а если набор правил не читается, обязательной считается каждая проверка. gate_open нужен хотя бы 1 опубликованный результат, 0 идущих и 0 блокирующих: ноль проверок — не ноль падений.", + "TASK": "Дайте идущей проверке завершиться зелёной; затем сделайте набор правил нечитаемым и посмотрите на списанную красную." + }, + "the-review-jam": { + "TITLE": "Затор ревью", + "GOAL": "В каком из десяти случаев застрявший пулл-реквест и почему побеждает правило, совпавшее раньше остальных.", + "TEXT": "Пулл-реквест может застрять сразу по нескольким причинам: отклонён, снова запушен и красный на шумовой проверке. review-jam.t27 даёт Королеве один ответ. jam_case проверяет 9 фактов в постоянном порядке, и побеждает правило, совпавшее раньше остальных, так что PR, который Королева не смогла измерить, никогда не считается свободным, а отклонённый и затем запушенный не принимается за шум. Случаев 10, от none до unmeasured.", + "TASK": "Снимайте факты по одному со стартового состояния и называйте каждый случай; затем найдите PR, который ни в каком заторе." + }, + "the-queens-move": { + "TITLE": "Ход Королевы", + "GOAL": "Что Королева делает в каждом заторе и каких заторов она ждёт.", + "TEXT": "У каждого случая одно действие на той же позиции: no reviewer отправляет ревьюера, refused not fixed отправляет кодера, refused then pushed отправляет повторное ревью, а labeled not merged завершает слияние. jam_action переводит любое значение больше 9 в measure again, так что неизвестный случай никогда не значит «делать нечего». jam_is_queens_turn ложно для 3 случаев — none, draft or fresh и stacked on an open PR: их она ждёт.", + "TASK": "Найдите случай, чьё действие — ждать базу; затем дотяните до 200 и прочитайте действие." + }, + "inherited-red": { + "TITLE": "Унаследованный красный", + "GOAL": "Когда красная проверка пулл-реквеста унаследована от master и почему отменённый запуск ничего не говорит.", + "TEXT": "Пчела может одобрить PR с красной проверкой, если эта проверка красная и в master. На 2026-10-06 PR слили красным, потому что запуск этой проверки в master был отменён и прочитан как красный. Теперь мерджер сам читает master, начиная с самого нового запуска: run_says учитывает только завершённое падение или успех, look_back пропускает всё остальное и запуски старше базы слияния, а discount_holds нужны слово пчелы и MASTER RED.", + "TASK": "Сделайте все запуски отменёнными; затем передвиньте падение раньше базы слияния и посмотрите на ворота." + }, + "a-refused-key": { + "TITLE": "Отклонённый ключ", + "GOAL": "Как Королева сортирует отказ провайдера: временный, квота, доступ или неверный запрос.", + "TEXT": "Пчёлы обращаются к моделям по ключам, и ключи отклоняют. key_failover.t27 сортирует каждый отказ функцией error_kind. Код z.ai в теле ответа важнее статуса HTTP, потому что z.ai отвечает одним и тем же 429 и на исчерпанную квоту, и на временную перегрузку: код 1305 временный, 1113 — квота. Отсутствие ответа — тоже временный отказ. Отказ по квоте или доступу закрывает ключ, а в счёт задачи идёт только неверный запрос.", + "TASK": "Сравните 429 с кодами 1308 и 1312; затем прочитайте, что значит отсутствие ответа." + }, + "a-key-rests": { + "TITLE": "Ключ отдыхает", + "GOAL": "Сколько отклонённый ключ отдыхает, прежде чем пчела сможет взять его снова.", + "TEXT": "Временный отказ даёт ключу отдохнуть COOL_BASE_SECONDS, 15 с, вдвое больше за каждый отказ подряд и не больше COOL_CAP_SECONDS, 240 с: 15, 30, 60, 120, 240. Исчерпанная квота или отклонённый ключ закрывают его на KEY_BREAK_SECONDS, 1800 с. Всё это даёт cooldown_seconds. Отдых короткий, чтобы 34 ключа никогда не отдыхали все сразу во время минутного всплеска, как на 2026-10-17.", + "TASK": "Найдите число отказов, после которого отдых перестаёт расти; затем переключите на квоту и прочитайте отдых." + }, + "the-next-key": { + "TITLE": "Следующий ключ", + "GOAL": "Какой ключ пчела пробует следующим и когда переходит к другому пулу или сдаётся.", + "TEXT": "На 2026-10-17 пчела начала на ключе 9 из 10 и погибла через 7 с, пока 33 других ключа простаивали: она получила один ключ при запуске и держала его. Теперь next_free берёт следующий после последнего ключ, который ещё не пробовали и который не отдыхает. 1305 — перегрузка модели пула, поэтому, когда POOL_OVERLOAD_KEYS, 3, ключа сказали это за минуту, step переводит пчелу в следующий пул. После 8 попыток ход заканчивается.", + "TASK": "Отметьте ключ 2 как опробованный и посмотрите, какой ключ получит кольцо; затем поставьте три ключа, ответивших 1305." + }, + "no-credit-for-keys": { + "TITLE": "Ключи не дают заслуг", + "GOAL": "Что приносят ключи, которые одолжил участник, и какие пределы их берегут.", + "TEXT": "Любой может одолжить улью ключи моделей через queen-contributor-keys.t27. Ключ проверяют, прежде чем включить: не больше 4 проверок сразу, новая проверка ждёт 60 с после прошлой, а у одного владельца не больше 100 ключей. Сам по себе ключ ничего не приносит: CREDIT_WALLET ложно, поэтому wallet_credit отвечает 0 при любом числе ключей. Опыт следует за работой, которую записывает Королева.", + "TASK": "Протяните число ключей от 0 до 100 и посмотрите на счётчик заслуг; затем найдите секунду, когда можно начать проверку." + }, + "twice-the-cost": { + "TITLE": "Вдвое дороже цены", + "GOAL": "Как проект назначает цену операции и что покупает месяц токенов.", + "TEXT": "Каждый доллар становится токенами по одному курсу, TOKEN_MICRO_USD 5000: токен — полцента. price_tokens продаёт операцию вдвое дороже её стоимости, MARKUP_PCT 200, и округляет вверх, чтобы ничего не продавалось дешевле: стоимость 12300 микродолларов — это 5 токенов. month_tokens превращает $20 в 4000 токенов, то есть 14 рилсов по 270 токенов или 333 портрета по 12.", + "TASK": "Найдите стоимости, которые дают ровно 1 токен; затем найдите месячную цену, на которую выходит 36 рилсов." + }, + "tokens-that-burn": { + "TITLE": "Сгорающие токены", + "GOAL": "Когда сгорает партия токенов и какая партия тратится раньше.", + "TEXT": "Правило владельца от 2026-10-01: все деньги уходят в токены, а токены, не потраченные за месяц, сгорают, так что заплативший делает контент, а не копит токены. Каждая партия живёт MONTH_DAYS, 30 дней, и days_left отсчитывает её от дня зачисления. На 25-й день у пакета месяца с дня 0 осталось 5 дней, у стартового подарка с дня 3 — 8, у пополнения с дня 12 — 17. Тратится раньше та, что сгорит раньше.", + "TASK": "Найдите день, когда сгорает пакет месяца; затем найдите последний день, когда остаётся хоть одна партия." + }, + "who-is-called": { + "TITLE": "Кого зовут", + "GOAL": "Какие сообщения в группе улья будят агента — от человека и от бота другого агента.", + "TEXT": "У каждого агента свой бот и своя тема в одной форумной группе. Человек зовёт агента его тегом, через @all, ответом на сообщение или просто сообщением в его теме. Бот другого агента зовёт его только тегом или ответом: бот никогда не поднимает весь улей и не будит хозяина темы, просто написав в ней. addressed в agent-hive-group.t27 — это правило длиной в одну строку.", + "TASK": "Найдите два способа, которыми агента может позвать только человек; затем включите все четыре и сравните отправителей." + }, + "three-bots-deep": { + "TITLE": "Три бота в глубину", + "GOAL": "Как улей не даёт агентам отвечать друг другу бесконечно.", + "TEXT": "Telegram не защищает ботов от зацикливания, так что два агента могли бы отвечать друг другу вечно. Это останавливают два числа. MAX_BOT_DEPTH равно 3: человек, затем A, B и C, затем тишина. BOT_REPLY_COOLDOWN_MS равно 5000: бот отвечает другому боту не чаще раза в 5 с. may_answer всегда пропускает человека, а бота — только внутри цепочки и после паузы.", + "TASK": "Найдите промежуток, при котором агент B замолкает; затем объясните, почему молчит и C." + }, + "stop-and-mute": { + "TITLE": "Stop и mute", + "GOAL": "Что делают /stop и /mute и почему ни один не теряет человека или прямой вызов.", + "TEXT": "cooled говорит, прошло ли 5000 мс с последнего сообщения бота. /stop глушит разговоры ботов между собой в чате на STOP_WINDOW_MS, 10 минут, а человека по-прежнему слышно: это may_answer_after_stop. /mute заставляет агента молчать в теме MUTE_MS, час, но на свой тег он отвечает: heard_while_muted. Оба — часы, поэтому никому не нужно включать улей обратно.", + "TASK": "Протяните часы /stop дальше 10 минут; затем позовите тегом заглушённого агента." + }, + "the-night-watch": { + "TITLE": "Ночной дозор", + "GOAL": "Что ночной дозор делает утром с четырьмя проверками карточек.", + "TEXT": "Каждую ночь идут четыре проверки чата карточек: тред, дверь статуса, слияния и паритет. night_verdict подводит итог: любое падение — это падение, иначе любое неизмеренное — неизмерено, иначе всё прошло. night_report превращает это в одно действие: падение открывает задачу или комментирует открытую, успех закрывает открытую задачу или молчит, а неизмеренная ночь оставляет всё как есть.", + "TASK": "Сделайте ночь успешной при открытой задаче; затем сделайте одну проверку неизмеренной." + }, + "the-public-board": { + "TITLE": "Публичная доска", + "GOAL": "В какой колонке публичная доска рисует каждое притязание и в каком случае ошибается.", + "TEXT": "Доска на app.t27.ai/game/kanban рисует каждый запуск в одной из 6 колонок. column_of читает притязание запуска на его задачу: running — в работе, accepted — готово, rejected и awaiting review — на ревью, failed — сброшено. dispatch.t27 записывает одну находку как есть: когда задача закрыта и список прочитан, любое завершённое притязание рисуется как готовое, каким бы ни был вердикт.", + "TASK": "Откройте задачу и посмотрите, куда уйдут четыре завершённых притязания; затем скажите, что скрывает закрытая задача." + }, + "the-comb-answers": { + "TITLE": "Соты отвечают", + "GOAL": "Как игра показывает, что с задачей что-то случилось, и как часто растёт карта.", + "TEXT": "В сотах игры у каждой задачи своя ячейка, и до hive.t27 принятая задача, потерянная аренда и отмена выглядели так же, как отсутствие новостей. cell_effect даёт каждому из 10 видов событий свою краску: старт белый, принявшее ревью золотое, возврат янтарный, потерянная аренда красная, отмена серая. flash_alpha гасит вспышку за FLASH_MS, 1800 мс. Новая ячейка перестраивает сцену, поэтому cells_due ставит новые ячейки пачками не чаще раза в 300 с.", + "TASK": "Нажмите ячейку ревью и переключите вердикт; затем найдите момент, когда вспышка гаснет." + } + } +} diff --git a/apps/website/public/learn/a-boundary-of-real-files/card.png b/apps/website/public/learn/a-boundary-of-real-files/card.png new file mode 100644 index 0000000000..4c4cbe82ec Binary files /dev/null and b/apps/website/public/learn/a-boundary-of-real-files/card.png differ diff --git a/apps/website/public/learn/a-boundary-of-real-files/index.html b/apps/website/public/learn/a-boundary-of-real-files/index.html new file mode 100644 index 0000000000..e951298165 --- /dev/null +++ b/apps/website/public/learn/a-boundary-of-real-files/index.html @@ -0,0 +1,205 @@ + + + + + + +A boundary of real files: the Queen and the hive course, lesson 2 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

A boundary of real files

+

You will learn

Which proposed paths a task may reserve, and why one invented path stops the whole shape.

+

A task's boundary is a reservation: while a bee works, no other bee takes those files. So a wrong boundary is worse than a missing one, because it holds files the work does not own. path_accepted takes any file that exists, and a new file only when its folder exists, the issue cites it, and its language is one the only-t27 rule allows. shape_lands writes the shape back only when no section is missing, it names 1 to 12 paths, and none of them was refused.

+

Try it

Make the two red paths green by changing only facts that could be true; then leave a section missing and watch the gate.

+

Open the interactive lesson →

+
A boundary: which files a task may reserve
A boundary: which files a task may reserve ↗
+

Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds.

+

specs/queen/shaping.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks
+; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix
+; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete.
+; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most
+; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as
+; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703,
+; #7642), which the rule counts as missing.
+; --
+; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section).
+; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a
+; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either
+; SHAPED by her (the missing sections are written by a model, every boundary path is checked against
+; the repository, and the body is written back only if judge() then returns 0), or, when shaping
+; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing.
+; --
+; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the
+; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is
+; accepted only when the issue itself cites it and its directory exists, and never when it would be a
+; new file in a language the only-t27 rule denies.
+; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenShaping;
+
+/// What the Queen does with one open issue.
+///   LEAVE  nothing: it is ready, a container, or already told
+///   SHAPE  ask for the missing sections, judge them, write them back if they pass
+///   TELL   comment once, naming what is missing
+pub const SH_LEAVE : u8 = 0;
+pub const SH_SHAPE : u8 = 1;
+pub const SH_TELL : u8 = 2;
+
+/// Attempts at shaping one issue before its author is told instead.
+pub const SHAPE_ATTEMPT_LIMIT : u32 = 2;
+/// Issues shaped per round. A shape is one model call; the round has other work.
+pub const SHAPES_PER_ROUND : u32 = 3;
+/// Minutes between two attempts on one issue, so a refused shape is not retried every round.
+pub const SHAPE_RETRY_MINUTES : u32 = 360;
+/// A boundary is a reservation; one naming more files than this is not a task, it is a project.
+pub const MAX_BOUNDARY_PATHS : u32 = 12;
+
+/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap;
+/// `attempts`: shapes tried so far; `told`: the author was already told.
+pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 {
+    if (missing == 0) { return SH_LEAVE; }
+    if (container) { return SH_LEAVE; }
+    if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; }
+    if (told) { return SH_LEAVE; }
+    return SH_TELL;
+}
+
+/// Is one proposed boundary path acceptable?
+///   exists       the file is in the repository at the base ref
+///   dir_exists   its directory is (a new file beside its siblings)
+///   cited        the issue's own title or body names this path
+///   foreign_new  it would be a NEW file whose extension the only-t27 rule denies to new work
+/// A file that exists is always acceptable: finding the file a title describes is the point. A file
+/// that does not exist must be cited by the issue, in a directory that exists, and not foreign.
+pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool {
+    if (exists) { return true; }
+    if (dir_exists == false) { return false; }
+    if (foreign_new) { return false; }
+    return cited;
+}
+
+/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and
+/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted.
+pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool {
+    if (missing_after != 0) { return false; }
+    if (paths == 0) { return false; }
+    if (paths > MAX_BOUNDARY_PATHS) { return false; }
+    return rejected == 0;
+}
+
+/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue.
+pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool {
+    if (attempts == 0) { return true; }
+    return minutes_since_last >= SHAPE_RETRY_MINUTES;
+}
+
+test a_ready_issue_and_a_container_are_left_alone {
+    assert shape_action(0, false, 0, false) == SH_LEAVE;
+    assert shape_action(15, true, 0, false) == SH_LEAVE;
+    assert shape_action(1, true, 9, false) == SH_LEAVE;
+}
+
+test an_unready_issue_is_shaped_then_its_author_is_told_once {
+    assert shape_action(1, false, 0, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE;
+    assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE;
+}
+
+test a_boundary_path_must_be_true_of_the_repository {
+    // an existing file, cited or found
+    assert path_accepted(true, true, false, false) == true;
+    assert path_accepted(true, true, true, false) == true;
+    // a new spec the issue asks for, beside its siblings
+    assert path_accepted(false, true, true, false) == true;
+    // a new file the issue never mentioned: invented
+    assert path_accepted(false, true, false, false) == false;
+    // a new file in a directory that does not exist
+    assert path_accepted(false, false, true, false) == false;
+    // a new .py or .ts the only-t27 rule denies, even when cited
+    assert path_accepted(false, true, true, true) == false;
+}
+
+test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds {
+    assert shape_lands(0, 1, 0) == true;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true;
+    assert shape_lands(1, 3, 0) == false;
+    assert shape_lands(0, 0, 0) == false;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false;
+    assert shape_lands(0, 3, 1) == false;
+}
+
+test a_refused_shape_is_not_retried_every_round {
+    assert attempt_due(0, 0) == true;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES) == true;
+}
+
+invariant the_codes_and_limits_never_move {
+    assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2;
+    assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0;
+    assert MAX_BOUNDARY_PATHS > 0;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/a-deploy-mid-flight/card.png b/apps/website/public/learn/a-deploy-mid-flight/card.png new file mode 100644 index 0000000000..7175d7596b Binary files /dev/null and b/apps/website/public/learn/a-deploy-mid-flight/card.png differ diff --git a/apps/website/public/learn/a-deploy-mid-flight/index.html b/apps/website/public/learn/a-deploy-mid-flight/index.html new file mode 100644 index 0000000000..ca7b1518fd --- /dev/null +++ b/apps/website/public/learn/a-deploy-mid-flight/index.html @@ -0,0 +1,396 @@ + + + + + + +A deploy mid-flight: the Queen and the hive course, lesson 9 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

A deploy mid-flight

+

You will learn

What a redeploy does to a bee, a review, a round, a job and a wait.

+

Every merge redeploys the Queen, and work is running when it does. drain.t27 sorts that work. A round, a job step and a wait row are rows the store keeps: the process leaves them and the next process carries on. A review is stopped, because a stop loses at most one review, which is redone. A bee is held, because what it has done lives in its conversation; at the cap, drain_cap_seconds, 1800 s today, it is handed back and its work is salvaged into its branch.

+

Try it

Drag the wait from 0 to the cap and watch the bee's row change; then cut the platform drain to 1000 s.

+

Open the interactive lesson →

+
A deploy mid-flight: what happens to a running bee
A deploy mid-flight: what happens to a running bee ↗
+

The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store.

+

specs/queen/drain.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/drain.t27 -- when a draining Queen process may exit: what it waits for, what it stops, what it leaves
+; Owner's night loop, 2026-10-10 (gHashTag/trios#1729 item 2): "every deploy still freezes the swarm for
+; 1800 s; with durable jobs and waits and stoppable turns, a spec can bound the drain by in-flight turns".
+; Measured before this card (gHashTag/trios trios/agent-server, branch queen):
+; - the drain is not a fixed wait. On SIGTERM the server drains its own bee runner (queen-runner.ts
+;   drainBeeRunner): it takes no new order and waits while any bee it carries still runs, up to
+;   TRIOS_BEE_RUNNER_DRAIN_SECONDS = 1800, then salvages the branch of each bee still running and exits
+;   about 2 s later. Rounds, the reviewer and the wait scheduler run on through that wait
+;   (roundsThroughDrain, BrowserOS#546) and are cut by the exit. Railway's
+;   RAILWAY_DEPLOYMENT_DRAINING_SECONDS = 1830 is only the SIGKILL that follows; the service has a
+;   volume, so the next container starts when this one ends.
+; - so the Queen's own bees are what the drain waits for, and they usually outlive it. GitHub's
+;   deployment statuses for the Queen's service, 2026-10-08 23:14Z to 2026-10-10 12:01Z: 7 of 8 deploys
+;   took 30.4 to 33.7 min from start to live, and one took 2.8 min (00:44Z, a quiet night).
+; - the 1800 s protects a bee that ends within 30 min of SIGTERM. A bee still running then is cut
+;   anyway (salvaged, its conversation lost), and its row stays claimed until its task lease lapses.
+; Owner rules this card keeps (2026-10-08): a deploy must not cut a bee short, and must not idle the
+; swarm; never shorten the drain without asking (a 120 s drain was tried and reverted in 5 min).
+; How others stop for a deploy: OTP stops children in reverse order with a shutdown time each, then
+; kills; Akka Cluster Sharding hands a shard off within 60 s and warns that Kubernetes' 30 s grace can
+; be shorter; Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds, then SIGKILL; DBOS,
+; Temporal and Restate keep a workflow's progress in the store, so a stopped process loses only the
+; step it was on.
+; What is reused, not restated:
+; - actors.t27 turn_signal (a turn's own bound, 0 meaning TURN_MAX_SECONDS) and stop_signal
+;   (X_SHUTDOWN, then X_KILL past a timeout);
+; - turn_stop.t27: a stopped turn gets its abort, TURN_STOP_GRACE_MS, then its escalation, and its lane
+;   is free within LANE_FREE_MS;
+; - waits.t27 section 7: a deploy writes nothing to a wait; the next process takes it up
+;   (pickup_after_seconds);
+; - jobs.t27: a job holds no lane, and an effect runs through the effects journal, so a process that
+;   stops mid-step never repeats it;
+; - control.t27 section 2: a runner's task lease lapses TASK_LEASE_TTL_SECONDS after its last renewal,
+;   and only then is its task reclaimable.
+; Not here: the store, the signals, the salvage (the runtime, gHashTag/trios).
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDrain;
+
+use queen::actors::turn_signal;
+use queen::actors::stop_signal;
+use queen::control::reclaimable;
+use queen::waits::pickup_after_seconds;
+
+; --- 1. What a draining process holds ---------------------------------------------------------------
+
+/// The kinds of work a Queen process can hold when SIGTERM arrives.
+/// WK_BEE: a bee this process runs for its runner (queen-runner.ts), a turn of up to an hour.
+/// WK_REVIEW: one review (reviewer.t27), a turn bounded by REVIEW_ROW_SECONDS.
+/// WK_ROUND: a Queen round, a reconcile: it re-reads the store and decides again.
+/// WK_JOB: a step of a job (jobs.t27).
+/// WK_WAIT: a wait row this process claimed (waits.t27).
+pub const WK_BEE : u8 = 0;
+pub const WK_REVIEW : u8 = 1;
+pub const WK_ROUND : u8 = 2;
+pub const WK_JOB : u8 = 3;
+pub const WK_WAIT : u8 = 4;
+pub const WORK_KINDS : u8 = 5;
+
+/// actors.t27 X_KILL and X_NONE, written as literals here (stop_signal and turn_signal answer them).
+pub const X_KILL_LIT : u8 = 2;
+pub const X_NONE_LIT : u8 = 99;
+
+/// Whether the store keeps this work as it stands, so another process continues it from there and
+/// a stop throws nothing away. A wait is a row a deploy never writes (waits.t27 section 7). A job is
+/// a row and its effects are journaled (jobs.t27). A round is a reconcile: everything it decided is
+/// decided again from the store by the next round, and every effect it made is journaled. A review
+/// and a bee are turns: their row stays, but what the turn has done so far is in its process.
+pub fn row_keeps(kind: u8) -> bool {
+    if (kind == WK_WAIT) { return true; }
+    if (kind == WK_JOB) { return true; }
+    if (kind == WK_ROUND) { return true; }
+    return false;
+}
+
+/// A turn's own bound in seconds, as actors.t27 turn_signal reads it (0 is TURN_MAX_SECONDS, 3600).
+/// A review is reviewer.t27 REVIEW_ROW_SECONDS (300, a literal here). A bee names none.
+pub const REVIEW_BOUND_SECONDS : u32 = 300;
+
+pub fn bound_of(kind: u8) -> u32 {
+    if (kind == WK_REVIEW) { return REVIEW_BOUND_SECONDS; }
+    return 0;
+}
+
+/// The most work a drain may throw away to end sooner: one review. A turn whose own timer kills it
+/// within this many seconds (turn_signal says X_KILL at this age) loses at most that much to a stop,
+/// and the next process redoes it from its row. A bee's turn does not qualify: its bound is an hour,
+/// and what it has done lives in its conversation, which no row keeps. Chosen, not measured.
+pub const STOP_LOSS_SECONDS : u32 = 300;
+
+pub fn redo_is_cheap(bound_seconds: u32) -> bool {
+    return turn_signal(STOP_LOSS_SECONDS, bound_seconds) == X_KILL_LIT;
+}
+
+; --- 2. What the drain owes each piece of work --------------------------------------------------------
+
+/// DV_DONE: its work ended; nothing is owed.
+/// DV_LEAVE: the store keeps it (row_keeps); the process exits without it and the next one takes it up.
+/// DV_STOP: the drain does not wait for it. It runs on while something else holds the process, and is
+/// stopped when the process exits, through turn_stop.t27 (its abort, the grace, the escalation).
+/// DV_HOLD: the drain waits for it to end on its own.
+/// DV_HAND_BACK: held to the cap and still running: it is stopped as DV_STOP is, what it wrote is
+/// salvaged into its branch and stored, and its row is ended now, so its issue is free at once.
+pub const DV_DONE : u8 = 0;
+pub const DV_LEAVE : u8 = 1;
+pub const DV_STOP : u8 = 2;
+pub const DV_HOLD : u8 = 3;
+pub const DV_HAND_BACK : u8 = 4;
+
+/// A bee is held until it ends or the cap passes: the owner's rule that a deploy does not cut a bee
+/// short, kept to the same cap as before this card. A review is not held: a stop loses at most one
+/// review, which the next process redoes. Rows are left.
+pub fn drain_verdict(kind: u8, ended: bool, waited_seconds: u32, cap_seconds: u32) -> u8 {
+    if (ended) { return DV_DONE; }
+    if (row_keeps(kind)) { return DV_LEAVE; }
+    if (redo_is_cheap(bound_of(kind))) { return DV_STOP; }
+    if (waited_seconds >= cap_seconds) { return DV_HAND_BACK; }
+    return DV_HOLD;
+}
+
+; --- 3. What keeps working while something holds ------------------------------------------------------
+
+/// Whether a source of work starts anything new during the drain. Nothing that would start a bee in
+/// this process: that bee would hold the drain for up to its hour. Everything else keeps going while
+/// a held bee keeps the process alive, because the next process cannot start before this one ends
+/// (one instance, one volume), and stopping the rounds then idled the swarm for the whole drain
+/// (measured 2026-10-08, BrowserOS#546). Once nothing holds, the process is leaving: nothing starts.
+pub fn takes_new_work(starts_a_bee_here: bool, holding: u32) -> bool {
+    if (starts_a_bee_here) { return false; }
+    return holding > 0;
+}
+
+; --- 4. The hard cap ----------------------------------------------------------------------------------
+
+/// What must still fit after the cap, before the platform's SIGKILL: the hand-back of every held bee
+/// (its abort and grace, the salvage into its branch, the bundle, the row), run side by side under one
+/// deadline, then the server's own close (main.ts gives it 2 s). Chosen, not measured.
+pub const HAND_BACK_SECONDS : u32 = 20;
+pub const EXIT_SECONDS : u32 = 5;
+
+/// The cap: the runner's drain (TRIOS_BEE_RUNNER_DRAIN_SECONDS), never so late that the hand-back and
+/// the exit cannot finish before the platform kills the process (RAILWAY_DEPLOYMENT_DRAINING_SECONDS).
+/// A platform drain of 0 is one this process cannot read: the runner's drain stands, as before.
+/// With today's 1800 and 1830 the cap is 1800: this card cuts no bee sooner than the drain it replaces.
+pub fn drain_cap_seconds(runner_drain_seconds: u32, platform_seconds: u32) -> u32 {
+    if (platform_seconds == 0) { return runner_drain_seconds; }
+    if (platform_seconds <= HAND_BACK_SECONDS + EXIT_SECONDS) { return 0; }
+    let latest : u32 = platform_seconds - HAND_BACK_SECONDS - EXIT_SECONDS;
+    if (runner_drain_seconds < latest) { return runner_drain_seconds; }
+    return latest;
+}
+
+; --- 5. The exit --------------------------------------------------------------------------------------
+
+/// turn_stop.t27's promises, written as literals here: the grace after the abort, and the time by
+/// which a stopped turn's lane is free.
+pub const TURN_STOP_GRACE_MS : u32 = 3000;
+pub const LANE_FREE_MS : u32 = 5000;
+
+pub const EXIT_NOW : u8 = 0;
+pub const EXIT_WAIT : u8 = 1;
+
+/// The process exits once nothing holds it and every turn it stopped has had its stop land: its abort
+/// at once, the grace, the escalation (stop_signal with the grace says X_KILL by LANE_FREE_MS). A
+/// process that stopped nothing exits at once.
+pub fn exit_step(holding: u32, stopping: u32, since_stop_ms: u32) -> u8 {
+    if (holding > 0) { return EXIT_WAIT; }
+    if (stopping == 0) { return EXIT_NOW; }
+    if (since_stop_ms >= LANE_FREE_MS) { return EXIT_NOW; }
+    return EXIT_WAIT;
+}
+
+; --- 6. When the work is free for another process -----------------------------------------------------
+
+/// control.t27 TASK_LEASE_TTL_SECONDS, written as a literal here (pinned by the invariant below).
+pub const TASK_LEASE_TTL_LIT : u32 = 180;
+
+/// Seconds after the exit before another process may take the issue of a bee the drain stopped.
+/// Handed back, its row is ended at the cap: free at once. Left claimed, as before this card, it waits
+/// until its task lease lapses (control.t27 reclaimable), counted from the last renewal, which stopped
+/// with the process.
+pub fn bee_free_after_seconds(handed_back: bool, since_renewal: u32) -> u32 {
+    if (handed_back) { return 0; }
+    if (reclaimable(false, since_renewal)) { return 0; }
+    return TASK_LEASE_TTL_LIT - since_renewal;
+}
+
+/// Seconds after the exit before another process may take a wait row this process had claimed: the
+/// rest of its claim (waits.t27 pickup_after_seconds). The drain writes nothing to it.
+pub fn wait_free_after_seconds(claimed: bool, since_claim: u32) -> u32 {
+    return pickup_after_seconds(claimed, since_claim);
+}
+
+; --- tests --------------------------------------------------------------------------------------------
+
+test rows_are_left_to_the_next_process {
+    assert row_keeps(WK_WAIT) == true;
+    assert row_keeps(WK_JOB) == true;
+    assert row_keeps(WK_ROUND) == true;
+    assert row_keeps(WK_REVIEW) == false;
+    assert row_keeps(WK_BEE) == false;
+    assert drain_verdict(WK_WAIT, false, 0, 1800) == DV_LEAVE;
+    assert drain_verdict(WK_JOB, false, 0, 1800) == DV_LEAVE;
+    assert drain_verdict(WK_ROUND, false, 0, 1800) == DV_LEAVE;
+    // past the cap too: a row is never handed back, it is simply there
+    assert drain_verdict(WK_WAIT, false, 1800, 1800) == DV_LEAVE;
+}
+
+test a_review_is_stopped_at_the_exit_not_waited_for {
+    assert bound_of(WK_REVIEW) == 300;
+    assert redo_is_cheap(300) == true;
+    assert redo_is_cheap(301) == false;
+    assert drain_verdict(WK_REVIEW, false, 0, 1800) == DV_STOP;
+    assert drain_verdict(WK_REVIEW, false, 1800, 1800) == DV_STOP;
+}
+
+test a_bee_is_held_until_it_ends_or_the_cap {
+    assert bound_of(WK_BEE) == 0;
+    // its bound is TURN_MAX_SECONDS: a stop could throw away an hour
+    assert redo_is_cheap(0) == false;
+    assert redo_is_cheap(3600) == false;
+    assert drain_verdict(WK_BEE, false, 0, 1800) == DV_HOLD;
+    assert drain_verdict(WK_BEE, false, 1799, 1800) == DV_HOLD;
+    assert drain_verdict(WK_BEE, false, 1800, 1800) == DV_HAND_BACK;
+    assert drain_verdict(WK_BEE, false, 4000, 1800) == DV_HAND_BACK;
+    // a bee that ended is owed nothing, at any time
+    assert drain_verdict(WK_BEE, true, 0, 1800) == DV_DONE;
+    assert drain_verdict(WK_BEE, true, 1800, 1800) == DV_DONE;
+    assert drain_verdict(WK_REVIEW, true, 0, 1800) == DV_DONE;
+    assert drain_verdict(WK_WAIT, true, 0, 1800) == DV_DONE;
+}
+
+test a_cap_of_zero_hands_every_bee_back_at_once {
+    assert drain_verdict(WK_BEE, false, 0, 0) == DV_HAND_BACK;
+}
+
+test nothing_starts_a_bee_here_and_the_rest_runs_only_while_held {
+    assert takes_new_work(true, 3) == false;
+    assert takes_new_work(true, 0) == false;
+    assert takes_new_work(false, 1) == true;
+    assert takes_new_work(false, 0) == false;
+}
+
+test the_cap_is_todays_drain_and_fits_the_platform {
+    // today: runner 1800, Railway 1830 -> 1800, no bee cut sooner than before
+    assert drain_cap_seconds(1800, 1830) == 1800;
+    // a platform drain too short for the hand-back moves the cap earlier
+    assert drain_cap_seconds(1800, 1800) == 1775;
+    assert drain_cap_seconds(1800, 26) == 1;
+    assert drain_cap_seconds(1800, 25) == 0;
+    assert drain_cap_seconds(1800, 10) == 0;
+    // a shorter runner drain is the owner's to choose, and it stands
+    assert drain_cap_seconds(600, 1830) == 600;
+    assert drain_cap_seconds(0, 1830) == 0;
+    // a platform drain this process cannot read leaves the runner's drain alone
+    assert drain_cap_seconds(1800, 0) == 1800;
+}
+
+test the_exit_waits_for_held_work_and_for_stops_to_land {
+    assert exit_step(1, 0, 0) == EXIT_WAIT;
+    assert exit_step(2, 3, 9000) == EXIT_WAIT;
+    assert exit_step(0, 0, 0) == EXIT_NOW;
+    assert exit_step(0, 2, 0) == EXIT_WAIT;
+    assert exit_step(0, 2, 4999) == EXIT_WAIT;
+    assert exit_step(0, 2, 5000) == EXIT_NOW;
+}
+
+test a_handed_back_bee_is_free_at_once_and_a_left_one_waits_out_its_lease {
+    assert bee_free_after_seconds(true, 0) == 0;
+    assert bee_free_after_seconds(true, 100) == 0;
+    assert bee_free_after_seconds(false, 0) == 180;
+    assert bee_free_after_seconds(false, 45) == 135;
+    assert bee_free_after_seconds(false, 179) == 1;
+    assert bee_free_after_seconds(false, 180) == 0;
+    assert bee_free_after_seconds(false, 4294967295) == 0;
+}
+
+test a_wait_row_is_free_after_the_rest_of_its_claim {
+    assert wait_free_after_seconds(false, 0) == 0;
+    assert wait_free_after_seconds(true, 0) == 60;
+    assert wait_free_after_seconds(true, 45) == 15;
+    assert wait_free_after_seconds(true, 60) == 0;
+}
+
+invariant the_verdicts_and_kinds_are_pinned {
+    assert DV_DONE == 0;
+    assert DV_LEAVE == 1;
+    assert DV_STOP == 2;
+    assert DV_HOLD == 3;
+    assert DV_HAND_BACK == 4;
+    assert WK_BEE == 0;
+    assert WK_WAIT == 4;
+    assert WORK_KINDS == 5;
+    assert EXIT_NOW == 0;
+    assert EXIT_WAIT == 1;
+}
+
+invariant the_literals_match_the_cards_they_come_from {
+    // actors.t27: turn_signal answers X_KILL at the bound and X_NONE before it
+    assert turn_signal(300, 300) == X_KILL_LIT;
+    assert turn_signal(299, 300) == X_NONE_LIT;
+    // control.t27: the task lease lapses at TASK_LEASE_TTL_SECONDS
+    assert reclaimable(false, TASK_LEASE_TTL_LIT) == true;
+    assert reclaimable(false, TASK_LEASE_TTL_LIT - 1) == false;
+    // turn_stop.t27: by LANE_FREE_MS a stopped turn's grace has passed (stop_signal says X_KILL)
+    assert stop_signal(LANE_FREE_MS, TURN_STOP_GRACE_MS) == X_KILL_LIT;
+    assert stop_signal(TURN_STOP_GRACE_MS - 1, TURN_STOP_GRACE_MS) == X_NONE_LIT;
+}
+
+invariant a_review_is_cheap_to_redo_and_a_bee_is_not {
+    assert STOP_LOSS_SECONDS == REVIEW_BOUND_SECONDS;
+    assert redo_is_cheap(REVIEW_BOUND_SECONDS) == true;
+    assert redo_is_cheap(0) == false;
+}
+
+invariant the_hand_back_fits_between_todays_drain_and_the_platform_kill {
+    assert HAND_BACK_SECONDS + EXIT_SECONDS <= 30;
+    assert drain_cap_seconds(1800, 1830) == 1800;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/a-key-rests/card.png b/apps/website/public/learn/a-key-rests/card.png new file mode 100644 index 0000000000..3c943b7456 Binary files /dev/null and b/apps/website/public/learn/a-key-rests/card.png differ diff --git a/apps/website/public/learn/a-key-rests/index.html b/apps/website/public/learn/a-key-rests/index.html new file mode 100644 index 0000000000..9c2fe30129 --- /dev/null +++ b/apps/website/public/learn/a-key-rests/index.html @@ -0,0 +1,458 @@ + + + + + + +A key rests: the Queen and the hive course, lesson 17 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

A key rests

+

You will learn

How long a refused key rests before a bee may use it again.

+

A transient refusal rests the key COOL_BASE_SECONDS, 15 s, doubled per strike in a row and never more than COOL_CAP_SECONDS, 240 s: 15, 30, 60, 120, 240. A spent quota or a refused key closes it for KEY_BREAK_SECONDS, 1800 s. cooldown_seconds gives all of it. The rest is short so that 34 keys are never all resting through a one-minute burst like the one of 2026-10-17.

+

Try it

Find the strike count where the rest stops growing; then switch to quota and read the rest.

+

Open the interactive lesson →

+
How long a refused key rests
How long a refused key rests ↗
+

A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/a-refused-key/card.png b/apps/website/public/learn/a-refused-key/card.png new file mode 100644 index 0000000000..700f969c9e Binary files /dev/null and b/apps/website/public/learn/a-refused-key/card.png differ diff --git a/apps/website/public/learn/a-refused-key/index.html b/apps/website/public/learn/a-refused-key/index.html new file mode 100644 index 0000000000..a35990896e --- /dev/null +++ b/apps/website/public/learn/a-refused-key/index.html @@ -0,0 +1,458 @@ + + + + + + +A refused key: the Queen and the hive course, lesson 16 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

A refused key

+

You will learn

How the Queen sorts a provider's refusal into transient, quota, auth or a bad request.

+

Bees call models with keys, and keys get refused. key_failover.t27 sorts each refusal with error_kind. The z.ai code in the body wins over the HTTP status, because z.ai sends a spent quota and a passing overload with the same 429: code 1305 is transient, 1113 is quota. No answer at all is transient. A quota or auth refusal closes the key, and only a bad request is charged to the issue.

+

Try it

Compare 429 with codes 1308 and 1312; then read what no answer at all is.

+

Open the interactive lesson →

+
A refused key: transient, quota, auth or a bad request
A refused key: transient, quota, auth or a bad request ↗
+

Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/a-tasks-life/card.png b/apps/website/public/learn/a-tasks-life/card.png new file mode 100644 index 0000000000..ad9ff2efd9 Binary files /dev/null and b/apps/website/public/learn/a-tasks-life/card.png differ diff --git a/apps/website/public/learn/a-tasks-life/index.html b/apps/website/public/learn/a-tasks-life/index.html new file mode 100644 index 0000000000..004d2d6798 --- /dev/null +++ b/apps/website/public/learn/a-tasks-life/index.html @@ -0,0 +1,669 @@ + + + + + + +A task's life: the Queen and the hive course, lesson 7 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

A task's life

+

You will learn

Which states a dispatched task passes through, and which events are legal in each.

+

dispatch.t27 is the Queen's task lifecycle, the cycle that chooses, starts, ends, reviews and releases work. A task has 12 states and 16 events. next_state answers every pair, and 99 means the event is illegal in that state: a verdict cannot reach a running task, and a reap cannot reach an ended one. After a verdict a send-back waits for its floor and is released; even an accept is sent back if CI turns red under the ceiling.

+

Try it

Walk a task from none to accept; then make CI red at the ceiling and find the one way out.

+

Open the interactive lesson →

+
A task's life: the Queen's state machine
A task's life: the Queen's state machine ↗
+

Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out.

+

specs/queen/dispatch.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done
+; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written
+; down in this family of repositories and only one of them moves work: the round of the trios supervisor
+; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it,
+; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with
+; the other cycles named as adapters and their gaps stated. Every decision below is a function the C
+; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py):
+;   ts      its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the
+;           public board's column, the single-flight round gate
+;   queend  its Swift policy binary, built at the pin and asked choose, review and retry
+;   pg      PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch
+;           writers on real rows, the criteria release, the CI take-back, the round's own SELECT
+;           (17.7 at this pin; the run at the old pin c25e1b02 used 16.15)
+;   lotus   gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which
+;           this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin
+;           c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host)
+;   live    one snapshot of the public endpoints of the deployed supervisor
+; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch
+; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read
+; from the source and named below: runner lanes (a claim in state queued that takes no worker slot,
+; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees)
+; and the public card's verdict and judgedHead.
+; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a
+; revision other than the pin. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDispatch;
+
+pub const KIND : str = "queen-dispatch";
+pub const ID : str = "queen/dispatch";
+pub const NAME : str = "The Queen's task lifecycle, from candidate to done";
+pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS";
+pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size";
+pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef";
+pub const CONSUMER_REPO : str = "gHashTag/trinity";
+pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
+pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530";
+pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149";
+; --- The decision: one canonical machine, the others are adapters ---------------------------------
+pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)";
+pub const ADAPTERS : [5]str = [
+    "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend",
+    "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design",
+    "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing",
+    "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task",
+    "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here"
+];
+pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state";
+pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"];
+; --- States of a dispatch row, and the events that move it ----------------------------------------
+pub const S_NONE : u32 = 0;
+pub const S_RUNNING : u32 = 1;
+pub const S_REFUSED : u32 = 2;
+pub const S_ENDED : u32 = 3;
+pub const S_WAIT : u32 = 4;
+pub const S_ACCEPT : u32 = 5;
+pub const S_SENDBACK : u32 = 6;
+pub const S_ESCALATE : u32 = 7;
+pub const S_EMPTY : u32 = 8;
+pub const S_STALE : u32 = 9;
+pub const S_REAPED : u32 = 10;
+pub const S_RELEASED : u32 = 11;
+pub const ILLEGAL : u32 = 99;
+pub const E_START : u32 = 0;
+pub const E_REFUSE : u32 = 1;
+pub const E_END : u32 = 2;
+pub const E_END_OTHER_TURN : u32 = 3;
+pub const E_REAP_STALL : u32 = 4;
+pub const E_REAP_BOOT : u32 = 5;
+pub const E_VERDICT_ACCEPT : u32 = 6;
+pub const E_VERDICT_SENDBACK : u32 = 7;
+pub const E_VERDICT_ESCALATE : u32 = 8;
+pub const E_VERDICT_WAIT : u32 = 9;
+pub const E_VERDICT_EMPTY : u32 = 10;
+pub const E_CRITERIA_CHANGED : u32 = 11;
+pub const E_CI_RED_UNDER_CEILING : u32 = 12;
+pub const E_CI_RED_AT_CEILING : u32 = 13;
+pub const E_FLOOR_PASSED : u32 = 14;
+pub const E_REDISPATCH : u32 = 15;
+pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)";
+pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation";
+pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it";
+; --- Claims: what a row costs its issue ------------------------------------------------------------
+pub const V_NONE : u32 = 0;
+pub const V_BLANK : u32 = 1;
+pub const V_WAIT : u32 = 2;
+pub const V_EMPTY : u32 = 3;
+pub const V_SENDBACK : u32 = 4;
+pub const V_ESCALATE : u32 = 5;
+pub const V_ACCEPT : u32 = 6;
+pub const V_FAILED : u32 = 7;
+pub const V_CANCELLED : u32 = 8;
+pub const V_STALE : u32 = 9;
+pub const V_OTHER : u32 = 10;
+pub const C_RUNNING : u32 = 0;
+pub const C_ACCEPTED : u32 = 1;
+pub const C_REJECTED : u32 = 2;
+pub const C_AWAITING_REVIEW : u32 = 3;
+pub const C_FAILED : u32 = 4;
+pub const EMPTY_FLOOR_MS : u64 = 1800000;
+pub const SENDBACK_FLOOR_MS : u64 = 3600000;
+pub const WAIT_FLOOR_MS : u64 = 21600000;
+pub const CEILING_RELEASE_MS : u64 = 3600000;
+pub const MAX_CEILING_RELEASES : u32 = 1;
+pub const SENDBACK_CEILING : u32 = 2;
+pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it";
+; --- Choosing ----------------------------------------------------------------------------------------
+pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates";
+pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies";
+pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing";
+pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only";
+pub const DEFAULT_WORKER_LIMIT : u32 = 4;
+pub const NONE_CHOSEN : u32 = 5;
+pub const COL_BACKLOG : u32 = 0;
+pub const COL_BLOCKED : u32 = 1;
+pub const COL_RUNNING : u32 = 2;
+pub const COL_REVIEW : u32 = 3;
+pub const COL_DONE : u32 = 4;
+pub const COL_DROPPED : u32 = 5;
+; --- Leases ------------------------------------------------------------------------------------------
+pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced";
+pub const LEASE_TTL_SECONDS : u32 = 180;
+pub const LEASE_HEARTBEAT_SECONDS : u32 = 60;
+pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane";
+pub const STALL_MINUTES : u32 = 120;
+; --- Review, retries, evidence, cancellation --------------------------------------------------------
+pub const R_ACCEPT : u32 = 0;
+pub const R_SENDBACK : u32 = 1;
+pub const R_ESCALATE : u32 = 2;
+pub const R_WAIT : u32 = 3;
+pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)";
+pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container";
+pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists";
+pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict";
+pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped";
+; --- Events, persistence, idempotency, projections --------------------------------------------------
+pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)";
+pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round";
+pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"];
+pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board";
+
+; --- End reasons for runs that produce no commits -----------------------------------------------------------
+; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted.
+; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days.
+pub const R_MODEL_ENDED_TURN : u32 = 0;      // Model ended its turn without editing any files
+pub const R_TIME_TOKEN_LIMIT : u32 = 1;     // Hit time or token limit during execution
+pub const R_CRASHED : u32 = 2;             // Container or tool crashed (with exit code)
+pub const R_BOUNDARY_CORRECT : u32 = 3;     // Boundary file was already correct, no changes needed
+pub const R_REFUSAL_START : u32 = 4;       // Refusal before the run started (invalid task, missing resources)
+pub const R_PUSH_FAILED : u32 = 5;          // Push to remote failed after successful run
+pub const R_UNKNOWN : u32 = 6;             // Cause could not be determined by the runner
+
+pub const END_REASON_COUNT : u32 = 7;
+pub const END_REASON_NAMES : [7]str = [
+    "model_ended_turn",
+    "time_token_limit", 
+    "crashed",
+    "boundary_correct",
+    "refusal_start",
+    "push_failed",
+    "unknown"
+];
+
+; --- Public board extension: show end reason on empty cards -----------------------------------------------
+; The public board's "empty" cards now include the end reason when available, enabling breakdown of
+; the 56.0% empty-branch share by reason from public endpoints alone.
+pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available";
+pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints";
+; --- Agents and workers ---------------------------------------------------------------------------------
+pub const AGENT_LETTERS : u32 = 27;
+pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27";
+; --- The specs this one sits beside ----------------------------------------------------------------------
+pub const NEIGHBOURS : [6]str = [
+    "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master",
+    "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort",
+    "specs/queen/brain_summaries.t27: does not compile in C at master",
+    "specs/brain/brain.t27: no function; does not compile in C at master",
+    "specs/brain/bus.t27: compiles, one test of a version constant",
+    "specs/brain/cognitive_loop.t27: compiles, one test of a version constant"
+];
+; --- Findings: measured, the tag names the evidence ------------------------------------------------------
+pub const FINDINGS_COUNT : u32 = 15;
+pub const FINDINGS : [15]str = [
+    "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]",
+    "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]",
+    "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]",
+    "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]",
+    "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]",
+    "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]",
+    "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]",
+    "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]",
+    "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]",
+    "f59 nothing cancels a running bee [source]",
+    "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]",
+    "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]",
+    "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]",
+    "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]",
+    "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]"
+];
+pub const ENABLED : bool = true;
+
+; --- Rules ------------------------------------------------------------------------------------------------------
+
+pub fn next_state(state: u32, event: u32) -> u32 {
+    if (state == S_NONE) {
+        if (event == E_START) { return S_RUNNING; }
+        if (event == E_REFUSE) { return S_REFUSED; }
+        return ILLEGAL;
+    }
+    if (state == S_RUNNING) {
+        if (event == E_END) { return S_ENDED; }
+        if (event == E_REAP_STALL) { return S_REAPED; }
+        if (event == E_REAP_BOOT) { return S_REAPED; }
+        return ILLEGAL;
+    }
+    if (state == S_ENDED or state == S_WAIT) {
+        if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; }
+        if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; }
+        if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; }
+        if (event == E_VERDICT_WAIT) { return S_WAIT; }
+        if (event == E_VERDICT_EMPTY) { return S_EMPTY; }
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ACCEPT) {
+        if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; }
+        if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; }
+        return ILLEGAL;
+    }
+    if (state == S_SENDBACK or state == S_EMPTY) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ESCALATE) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        return ILLEGAL;
+    }
+    if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) {
+        if (event == E_REDISPATCH) { return S_RUNNING; }
+        return ILLEGAL;
+    }
+    return ILLEGAL;
+}
+
+pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 {
+    if (finished == false) { return C_RUNNING; }
+    if (verdict == V_ACCEPT) { return C_ACCEPTED; }
+    if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; }
+    if (verdict == V_EMPTY) {
+        if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_STALE) { return C_FAILED; }
+    if (verdict == V_SENDBACK) {
+        if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+        if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; }
+        if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) {
+        if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+    }
+    return C_AWAITING_REVIEW;
+}
+
+; What the round and the board actually pass for `releases`: neither selects the column.
+pub fn releases_read_by_round(stored: u32) -> u32 {
+    return 0;
+}
+
+pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 {
+    if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; }
+    if (claim == C_RUNNING) { return COL_RUNNING; }
+    if (claim == C_ACCEPTED) { return COL_DONE; }
+    if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; }
+    return COL_DROPPED;
+}
+
+pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 {
+    if (e0) { return 0; }
+    if (e1) { return 1; }
+    if (e2) { return 2; }
+    if (e3) { return 3; }
+    if (e4) { return 4; }
+    return NONE_CHOSEN;
+}
+
+pub fn can_start(running: u32, limit: u32) -> bool {
+    return running < limit;
+}
+
+pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 {
+    if (total == 0) { return R_ESCALATE; }
+    if (judged < total) { return R_WAIT; }
+    if (met == total) {
+        if (committed_known and committed > 0) { return R_ACCEPT; }
+        return R_ESCALATE;
+    }
+    if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; }
+    return R_SENDBACK;
+}
+
+pub fn retry_allowed(real_attempts: u32) -> bool {
+    return real_attempts < SENDBACK_CEILING;
+}
+
+pub fn counts_against_issue(interrupted: bool) -> bool {
+    return interrupted == false;
+}
+
+pub fn lease_taken(expired: bool, same_holder: bool) -> bool {
+    return expired or same_holder;
+}
+
+pub fn fence_after(fence: u64) -> u64 {
+    return fence + 1;
+}
+
+pub fn owners_per_term() -> u32 {
+    return 1;
+}
+
+pub fn stall_reapable(age_minutes: u32) -> bool {
+    return age_minutes > STALL_MINUTES;
+}
+
+pub fn followup_rounds(requests_during_round: u32) -> u32 {
+    if (requests_during_round > 0) { return 1; }
+    return 0;
+}
+
+pub fn accept_requires_pull_request() -> bool {
+    return false;
+}
+
+pub fn writer_enforces_table() -> bool {
+    return false;
+}
+
+invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; }
+invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; }
+invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; }
+invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; }
+invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); }
+invariant all_end_reasons_valid { 
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN);
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT);
+    assert is_valid_end_reason(R_CRASHED);
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT);
+    assert is_valid_end_reason(R_REFUSAL_START);
+    assert is_valid_end_reason(R_PUSH_FAILED);
+    assert is_valid_end_reason(R_UNKNOWN);
+}
+
+; --- End reason handling functions --------------------------------------------------------------------------
+
+pub fn is_valid_end_reason(reason: u32) -> bool {
+    return reason < END_REASON_COUNT;
+}
+
+pub fn end_reason_to_name(reason: u32) -> str {
+    if (is_valid_end_reason(reason)) {
+        return END_REASON_NAMES[reason];
+    }
+    return "invalid_end_reason";
+}
+
+pub fn is_no_diff_end_reason(reason: u32) -> bool {
+    return reason == R_MODEL_ENDED_TURN 
+        or reason == R_TIME_TOKEN_LIMIT 
+        or reason == R_BOUNDARY_CORRECT 
+        or reason == R_REFUSAL_START 
+        or reason == R_PUSH_FAILED 
+        or reason == R_UNKNOWN;
+}
+
+pub fn is_crash_end_reason(reason: u32) -> bool {
+    return reason == R_CRASHED;
+}
+
+test the_table_moves_only_along_its_guards {
+    assert next_state(S_NONE, E_START) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END) == S_ENDED;
+    assert next_state(S_ENDED, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE;
+    assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL;
+    assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL;
+    // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886).
+    assert next_state(S_NONE, E_REFUSE) == S_REFUSED;
+    assert next_state(S_NONE, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED;
+    assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED;
+    assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK;
+    assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE;
+    assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT;
+    assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY;
+    assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK;
+    assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_START) == ILLEGAL;
+    assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING;
+}
+
+test a_claim_follows_the_verdict_and_three_clocks {
+    assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING;
+    assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED;
+    assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED;
+    assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW;
+    // #9886: each verdict that frees the issue, the send-back floor at its edge with a release
+    // already spent, and the wait floor's two edges.
+    assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED;
+    assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW;
+    assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW;
+}
+
+test a_closed_issue_is_drawn_done_whatever_its_verdict {
+    assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE;
+    assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW;
+    assert column_of(C_RUNNING, false, true) == COL_RUNNING;
+    assert column_of(C_FAILED, true, true) == COL_DROPPED;
+    assert column_of(C_ACCEPTED, true, true) == COL_DONE;
+}
+
+test the_first_eligible_wins_in_any_order {
+    assert chosen_of(false, false, true, true, false) == 2;
+    assert chosen_of(false, true, false, false, true) == 1;
+    assert chosen_of(false, false, false, false, false) == NONE_CHOSEN;
+    assert chosen_of(true, true, false, false, false) == 0;
+    assert chosen_of(false, false, false, true, true) == 3;
+    assert chosen_of(false, false, false, false, true) == 4;
+    assert can_start(3, DEFAULT_WORKER_LIMIT) == true;
+    assert can_start(4, DEFAULT_WORKER_LIMIT) == false;
+}
+
+test review_escalates_without_work_and_after_two_send_backs {
+    assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE;
+    assert review_of(2, 1, 1, true, 1, 0) == R_WAIT;
+    assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT;
+    assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK;
+    assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE;
+    assert retry_allowed(1) == true;
+    assert retry_allowed(2) == false;
+    assert counts_against_issue(true) == false;
+    assert counts_against_issue(false) == true;
+}
+
+test one_lease_one_owner_and_age_alone_reaps {
+    assert lease_taken(false, false) == false;
+    assert lease_taken(true, false) == true;
+    assert lease_taken(false, true) == true;
+    assert fence_after(3) == 4;
+    assert owners_per_term() == 1;
+    assert stall_reapable(121) == true;
+    assert stall_reapable(119) == false;
+    // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself.
+    assert stall_reapable(120) == false;
+    assert followup_rounds(5) == 1;
+    assert followup_rounds(0) == 0;
+    assert accept_requires_pull_request() == false;
+    assert writer_enforces_table() == false;
+}
+
+; --- End reason tests --------------------------------------------------------------------------------------
+
+test end_reason_constants_are_correct {
+    assert R_MODEL_ENDED_TURN == 0;
+    assert R_TIME_TOKEN_LIMIT == 1;
+    assert R_CRASHED == 2;
+    assert R_BOUNDARY_CORRECT == 3;
+    assert R_REFUSAL_START == 4;
+    assert R_PUSH_FAILED == 5;
+    assert R_UNKNOWN == 6;
+    assert END_REASON_COUNT == 7;
+    // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two
+    // call the functions that read the constants: the last reason is named, one past it is not.
+    assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown";
+    assert is_valid_end_reason(END_REASON_COUNT) == false;
+}
+
+test end_reason_names_array_matches_constants {
+    assert END_REASON_NAMES[0] == "model_ended_turn";
+    assert END_REASON_NAMES[1] == "time_token_limit";
+    assert END_REASON_NAMES[2] == "crashed";
+    assert END_REASON_NAMES[3] == "boundary_correct";
+    assert END_REASON_NAMES[4] == "refusal_start";
+    assert END_REASON_NAMES[5] == "push_failed";
+    assert END_REASON_NAMES[6] == "unknown";
+}
+
+test end_reason_validation_works {
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_valid_end_reason(R_CRASHED) == true;
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_valid_end_reason(R_REFUSAL_START) == true;
+    assert is_valid_end_reason(R_PUSH_FAILED) == true;
+    assert is_valid_end_reason(R_UNKNOWN) == true;
+    assert is_valid_end_reason(7) == false;  // One past the last valid reason
+    assert is_valid_end_reason(99) == false; // Way past the last valid reason
+}
+
+test end_reason_to_name_conversion {
+    assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn";
+    assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit";
+    assert end_reason_to_name(R_CRASHED) == "crashed";
+    assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct";
+    assert end_reason_to_name(R_REFUSAL_START) == "refusal_start";
+    assert end_reason_to_name(R_PUSH_FAILED) == "push_failed";
+    assert end_reason_to_name(R_UNKNOWN) == "unknown";
+    assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string
+}
+
+test no_diff_end_reason_classification {
+    assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_no_diff_end_reason(R_REFUSAL_START) == true;
+    assert is_no_diff_end_reason(R_PUSH_FAILED) == true;
+    assert is_no_diff_end_reason(R_UNKNOWN) == true;
+    assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category
+}
+
+test crash_end_reason_classification {
+    assert is_crash_end_reason(R_CRASHED) == true;
+    assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false;
+    assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false;
+    assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false;
+    assert is_crash_end_reason(R_REFUSAL_START) == false;
+    assert is_crash_end_reason(R_PUSH_FAILED) == false;
+    assert is_crash_end_reason(R_UNKNOWN) == false;
+}
+
+test end_reason_enables_breakdown_of_empty_branches {
+    // This test validates that the end reason vocabulary enables breaking down
+    // the 56.0% empty-branch share by reason from public endpoints alone
+    
+    // Simulate counts for empty branches by reason
+    var model_ended_count : u32 = 100;
+    var token_limit_count : u32 = 80;
+    var boundary_correct_count : u32 = 60;
+    var refusal_start_count : u32 = 40;
+    var push_failed_count : u32 = 30;
+    var unknown_count : u32 = 7;  // Should be < 10% of total
+    var crash_count : u32 = 0;
+    
+    var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                           refusal_start_count + push_failed_count + unknown_count + crash_count;
+    
+    // Verify unknown is less than 10% of total (success criterion from issue)
+    assert unknown_count < total_empty / 10;
+    
+    // Verify we can calculate breakdown percentages
+    var model_percentage : u32 = (model_ended_count * 100) / total_empty;
+    var token_percentage : u32 = (token_limit_count * 100) / total_empty;
+    var unknown_percentage : u32 = (unknown_count * 100) / total_empty;
+    
+    assert model_percentage > 0;
+    assert token_percentage > 0;
+    assert unknown_percentage < 10;  // Unknown should be < 10%
+    
+    // Verify no-diff vs crash categorization works
+    var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                              refusal_start_count + push_failed_count + unknown_count;
+    var crash_total : u32 = crash_count;
+    
+    assert no_diff_total == total_empty - crash_total;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/aging-a-label/card.png b/apps/website/public/learn/aging-a-label/card.png new file mode 100644 index 0000000000..f6ed1810fc Binary files /dev/null and b/apps/website/public/learn/aging-a-label/card.png differ diff --git a/apps/website/public/learn/aging-a-label/index.html b/apps/website/public/learn/aging-a-label/index.html new file mode 100644 index 0000000000..6b685837a1 --- /dev/null +++ b/apps/website/public/learn/aging-a-label/index.html @@ -0,0 +1,238 @@ + + + + + + +Aging a label: the Queen and the hive course, lesson 4 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Aging a label

+

You will learn

How a labelled issue moves up with age, and why only by one level.

+

Without labels the Queen takes issues in GitHub's listing order. priority.t27 adds four levels, critical, high, normal and low; lower runs first, and an unlabelled issue counts as normal. A labelled issue gains one level after AGING_DAYS, 14 days, and at most MAX_AGING_STEPS, 1 level, so a low issue reaches normal on day 14 and stays there. High and critical do not age. effective_level computes the line the widget draws.

+

Try it

Find the day a labelled low issue moves up; then remove the label and drag to day 60.

+

Open the interactive lesson →

+
Aging: how long a labelled low issue waits
Aging: how long a labelled low issue waits ↗
+

Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical.

+

specs/queen/priority.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366)
+; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no
+; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it.
+; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js`
+; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes.
+; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank,
+; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort):
+;   - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first);
+;   - an issue with an open blocker is skipped, not waited on (head-of-line blocking);
+;   - a cap on critical stops priority inflation; aging stops starvation of labelled low work;
+;   - an issue nobody labelled keeps today's place, so a repository with no labels sees no change.
+; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority
+; field (read it first once the org enables it; its four values map onto the same levels).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenPriority;
+
+/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages.
+pub const LEVEL_CRITICAL : u8 = 0;
+pub const LEVEL_HIGH : u8 = 1;
+pub const LEVEL_NORMAL : u8 = 2;
+pub const LEVEL_LOW : u8 = 3;
+
+/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing.
+/// When an issue carries labels of two levels, the more urgent one counts.
+pub const CRITICAL_LABELS : str = "P0,priority/critical,critical";
+pub const HIGH_LABELS : str = "P1,priority/high,high-priority";
+pub const NORMAL_LABELS : str = "P2,priority/medium";
+pub const LOW_LABELS : str = "P3,P4,priority/low";
+
+/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels,
+/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low"
+/// the fastest way to the front of a long queue.
+pub const AGING_DAYS : u32 = 14;
+pub const MAX_AGING_STEPS : u32 = 1;
+
+/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH.
+pub const CRITICAL_CAP : u32 = 3;
+
+/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs.
+pub const WHY_LISTING : u8 = 0;
+pub const WHY_LABEL : u8 = 1;
+pub const WHY_AGED : u8 = 2;
+pub const WHY_CAPPED : u8 = 3;
+pub const WHY_BLOCKED : u8 = 4;
+
+/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one.
+pub fn capped_level(base: u8, criticals_before: u32) -> u8 {
+    if (base == LEVEL_CRITICAL) {
+        if (criticals_before >= CRITICAL_CAP) {
+            return LEVEL_HIGH;
+        }
+    }
+    return base;
+}
+
+/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS)
+/// when it is labelled.
+pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 {
+    if (labelled == false) {
+        return level;
+    }
+    if (level <= LEVEL_HIGH) {
+        return level;
+    }
+    var steps : u32 = age_days / AGING_DAYS;
+    if (steps > MAX_AGING_STEPS) {
+        steps = MAX_AGING_STEPS;
+    }
+    var gap : u32 = (level as u32) - (LEVEL_HIGH as u32);
+    if (steps >= gap) {
+        return LEVEL_HIGH;
+    }
+    return ((level as u32) - steps) as u8;
+}
+
+/// An issue with an open blocker is not a candidate; the Queen moves past it.
+pub fn eligible(open_blockers: u32) -> bool {
+    return open_blockers == 0;
+}
+
+/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged
+/// issue never beats a real one of the same level), then earlier in GitHub's listing.
+pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool {
+    if (eff_a < eff_b) { return true; }
+    if (eff_a > eff_b) { return false; }
+    if (level_a < level_b) { return true; }
+    if (level_a > level_b) { return false; }
+    return index_a < index_b;
+}
+
+/// The reason printed for one issue.
+pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 {
+    if (open_blockers > 0) { return WHY_BLOCKED; }
+    if (level != base) { return WHY_CAPPED; }
+    if (eff != level) { return WHY_AGED; }
+    if (labelled) { return WHY_LABEL; }
+    return WHY_LISTING;
+}
+
+test no_label_keeps_the_listing_order {
+    assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1);
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false;
+    // #7077: a strict order, so a sort can use it: no issue outranks itself
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false;
+}
+
+test critical_runs_before_a_newer_unlabelled_issue {
+    assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0);
+}
+
+test unlabelled_never_beats_urgent {
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false;
+}
+
+test labelled_work_ages_one_level_and_never_to_critical {
+    assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW;
+    assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH;
+    assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH;
+}
+
+test an_aged_issue_loses_the_tie_to_a_real_one {
+    var aged : u8 = effective_level(LEVEL_LOW, true, 28);
+    assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0);
+    // #7077: of two LOW labels the aged one goes first, whatever the listing says
+    assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0);
+    assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false;
+}
+
+test the_fourth_critical_runs_as_high {
+    assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH;
+    assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW;
+}
+
+test a_blocked_issue_is_skipped {
+    assert eligible(0);
+    assert eligible(1) == false;
+    assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED;
+}
+
+test every_reason_is_reachable {
+    assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING;
+    assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL;
+    assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED;
+    assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED;
+}
+
+invariant levels_are_ordered {
+    assert LEVEL_CRITICAL < LEVEL_HIGH;
+    assert LEVEL_HIGH < LEVEL_NORMAL;
+    assert LEVEL_NORMAL < LEVEL_LOW;
+    assert CRITICAL_CAP > 0;
+    assert AGING_DAYS > 0;
+    assert MAX_AGING_STEPS == 1;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/back-off/card.png b/apps/website/public/learn/back-off/card.png new file mode 100644 index 0000000000..be1973acd4 Binary files /dev/null and b/apps/website/public/learn/back-off/card.png differ diff --git a/apps/website/public/learn/back-off/index.html b/apps/website/public/learn/back-off/index.html new file mode 100644 index 0000000000..f9b08c310f --- /dev/null +++ b/apps/website/public/learn/back-off/index.html @@ -0,0 +1,390 @@ + + + + + + +Back off: the Queen and the hive course, lesson 11 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Back off

+

You will learn

Why a row that answers wait is visited less and less often, and how often at least.

+

Over 44 minutes on 2026-10-09 the reviewer made 941 row visits, 888 of them wait, from 22 rows whose branch it could not read; one row was visited 54 times. revisit_after_seconds now waits 30 s after one wait and doubles per wait, never past 900 s: 30, 60, 120, 240, 480, 900. due_again lets the row be taken only once that time has passed, and any other answer resets the count.

+

Try it

Find the wait count where the pause stops growing; then set 4 waits and find the second the row is due.

+

Open the interactive lesson →

+
Back off: when the reviewer looks at a waiting row again
Back off: when the reviewer looks at a waiting row again ↗
+

A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due.

+

specs/queen/reviewer.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time
+; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the
+; live Queen (gHashTag/BrowserOS trios/agent-server):
+; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91);
+; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after
+;   another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to
+;   2 minutes -- in practice about one review a round;
+; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min).
+; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each
+; on its own lane, one row never twice at once, and every waiting row visited in turn. The round
+; then reviews nothing: two sweeps over one row would race on its verdict.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewer;
+
+/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy
+/// costs one query.
+pub const REVIEWER_EVERY_SECONDS : u32 = 10;
+
+/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the
+/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds).
+/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays
+/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour.
+pub const REVIEWER_CONCURRENCY : u32 = 4;
+
+/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and
+/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`).
+pub const REVIEWER_CEILING : u32 = 16;
+
+/// Seconds in an hour; finish rates are measured per hour.
+pub const SECONDS_PER_HOUR : u32 = 3600;
+
+/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer
+/// shares a 45 s budget with the round, because it no longer holds the round up.
+pub const REVIEW_ROW_SECONDS : u32 = 300;
+
+/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58
+/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS
+/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below
+/// the finish rate even if every review used its whole budget, and the backlog only grows.
+/// How many rows the reviewer may take at once, from what the host measures:
+/// - `keys` model keys, each with `lanes_per_key` lanes free for review;
+/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review);
+/// - never above REVIEWER_CEILING, never above the rows `waiting`;
+/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains.
+pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 {
+    if (waiting == 0) { return 0; }
+    var k : u32 = keys;
+    if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; }
+    var l : u32 = lanes_per_key;
+    if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; }
+    var bound : u32 = k * l;
+    if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; }
+    var by_memory : u32 = 1;
+    if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; }
+    if (by_memory < bound) { bound = by_memory; }
+    if (waiting < bound) { bound = waiting; }
+    if (bound < 1) { return 1; }
+    return bound;
+}
+
+/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every
+/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600).
+/// Split by the hour so no u32 product overflows.
+pub fn slots_needed(finished_per_hour: u32) -> u32 {
+    let whole : u32 = finished_per_hour / SECONDS_PER_HOUR;
+    let part : u32 = finished_per_hour % SECONDS_PER_HOUR;
+    let part_seconds : u32 = part * REVIEW_ROW_SECONDS;
+    var extra : u32 = part_seconds / SECONDS_PER_HOUR;
+    if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; }
+    return whole * REVIEW_ROW_SECONDS + extra;
+}
+
+/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows.
+pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool {
+    return slots >= slots_needed(finished_per_hour);
+}
+
+/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but
+/// no more than the rows waiting.
+pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 {
+    if (in_flight >= bound) { return 0; }
+    let room : u32 = bound - in_flight;
+    if (waiting < room) { return waiting; }
+    return room;
+}
+
+/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed
+/// REVIEWER_CONCURRENCY, unchanged for its existing callers.
+pub fn review_slots(in_flight: u32, waiting: u32) -> u32 {
+    return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting);
+}
+
+/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never
+/// visited before any it has. Without it, a row that is re-read and waits again (no review spent)
+/// keeps its place at the head and the reviewer circles the same few rows.
+pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool {
+    if (never_a and never_b == false) { return true; }
+    if (never_b and never_a == false) { return false; }
+    return seconds_since_a > seconds_since_b;
+}
+
+/// While the reviewer runs, the round reviews nothing.
+pub fn round_reviews(reviewer_running: bool) -> bool {
+    return reviewer_running == false;
+}
+
+/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen,
+/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could
+/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row
+/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned
+/// about 430 visits of the reviewer's slots.
+/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never
+/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that
+/// has started to read is visited at the normal pace again.
+pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30;
+pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900;
+
+pub fn revisit_after_seconds(waits: u32) -> u32 {
+    if (waits == 0) { return 0; }
+    var wait : u32 = WAIT_REVISIT_BASE_SECONDS;
+    var n : u32 = 1;
+    while (n < waits) {
+        if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+    return wait;
+}
+
+/// Whether a row may be taken now: one that never waited always may, and one that waited only after
+/// its revisit time has passed since the last visit.
+pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool {
+    return seconds_since_visit >= revisit_after_seconds(waits);
+}
+
+/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the
+/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait`
+/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only
+/// when the bee committed something, so a bee that committed nothing leaves no branch here, the
+/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed.
+/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free
+/// again after the retry floor.
+/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle
+/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit,
+/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty
+/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits,
+/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner`
+/// false) and a row whose bundle is stored are never decided here: their diff failed for some
+/// other reason, and that still decides nothing.
+pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10;
+
+pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool {
+    if (on_runner == false) { return false; }
+    if (bundle_stored) { return false; }
+    return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES;
+}
+
+test a_pass_fills_the_free_slots_with_waiting_rows {
+    assert review_slots(0, 0) == 0;
+    assert review_slots(0, 2) == 2;
+    assert review_slots(0, 100) == REVIEWER_CONCURRENCY;
+    assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1;
+    assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1;
+    assert review_slots(REVIEWER_CONCURRENCY, 100) == 0;
+    assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0;
+}
+
+test the_bound_follows_lanes_memory_and_backlog {
+    // nothing waits: no slot, whatever the host has
+    assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0;
+    // lanes bound: 2 keys x 3 lanes
+    assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6;
+    // memory bound: 1000 MB at 300 MB a review
+    assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3;
+    // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free
+    assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16;
+    assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING;
+    // never above the backlog
+    assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2;
+    // at least one while anything waits, even with no lane or memory measured
+    assert reviewer_concurrency(0, 0, 0, 0, 1) == 1;
+    assert reviewer_concurrency(10, 10, 100, 300, 111) == 1;
+    assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1;
+    assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1;
+}
+
+test the_measured_load_needs_five_and_the_bound_admits_them {
+    // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5
+    assert slots_needed(58) == 5;
+    assert slots_needed(0) == 0;
+    assert slots_needed(12) == 1;
+    assert slots_needed(13) == 2;
+    assert slots_needed(3600) == REVIEW_ROW_SECONDS;
+    assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1;
+    assert slots_needed(4294967295) == 357913942;
+    // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true;
+    assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16;
+    assert keeps_up(4, 48) == true;
+    assert keeps_up(4, 49) == false;
+}
+
+test a_pass_fills_the_free_slots_against_the_computed_bound {
+    let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111);
+    assert review_slots_within(bound, 0, 111) == 16;
+    assert review_slots_within(bound, 11, 111) == 5;
+    assert review_slots_within(bound, 15, 3) == 1;
+    assert review_slots_within(bound, 16, 111) == 0;
+    assert review_slots_within(bound, 17, 111) == 0;
+    assert review_slots_within(bound, 0, 3) == 3;
+    assert review_slots_within(0, 0, 111) == 0;
+}
+
+test every_row_is_visited_in_turn {
+    // never visited goes first
+    assert visit_first(true, 0, false, 9999) == true;
+    assert visit_first(false, 9999, true, 0) == false;
+    // otherwise the one visited longest ago
+    assert visit_first(false, 300, false, 20) == true;
+    assert visit_first(false, 20, false, 300) == false;
+    assert visit_first(false, 20, false, 20) == false;
+    assert visit_first(true, 0, true, 0) == false;
+}
+
+test the_round_stands_aside_for_the_reviewer {
+    assert round_reviews(true) == false;
+    assert round_reviews(false) == true;
+}
+
+test a_row_that_keeps_waiting_is_visited_less_and_less {
+    assert revisit_after_seconds(0) == 0;
+    assert revisit_after_seconds(1) == 30;
+    assert revisit_after_seconds(2) == 60;
+    assert revisit_after_seconds(3) == 120;
+    assert revisit_after_seconds(5) == 480;
+    assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS;
+    // a row that never waited is always due; one that waited is due only once its time has passed
+    assert due_again(0, 0) == true;
+    assert due_again(1, 29) == false;
+    assert due_again(1, 30) == true;
+    assert due_again(6, 899) == false;
+    assert due_again(6, 900) == true;
+}
+
+test no_bundle_is_empty_for_a_runner_row_past_the_grace {
+    // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true;
+    // the six hours the frozen-wait valve would otherwise have held it
+    assert no_bundle_is_empty(true, false, 360) == true;
+    assert no_bundle_is_empty(true, false, 4294967295) == true;
+}
+
+test no_bundle_is_empty_waits_inside_the_grace {
+    // just finished: the runner may not have stored its bundle yet
+    assert no_bundle_is_empty(true, false, 0) == false;
+    // a bundle 40 s late is still awaited, and judged on its branch when it arrives
+    assert no_bundle_is_empty(true, false, 1) == false;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false;
+}
+
+test no_bundle_is_empty_leaves_a_container_row_alone {
+    // a bee that ran here has no bundle by design; its diff failing decides nothing
+    assert no_bundle_is_empty(false, false, 0) == false;
+    assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(false, false, 4294967295) == false;
+    assert no_bundle_is_empty(false, true, 4294967295) == false;
+}
+
+test no_bundle_is_empty_leaves_a_stored_bundle_alone {
+    // the bundle is there, so the diff failed for another reason: still a wait
+    assert no_bundle_is_empty(true, true, 0) == false;
+    assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(true, true, 4294967295) == false;
+}
+
+invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour {
+    // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit
+    assert NO_BUNDLE_GRACE_MINUTES > 1;
+    assert no_bundle_is_empty(true, false, 1) == false;
+    // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit
+    // that judges it comes within the hour of its finish, not after the six-hour valve
+    assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR;
+}
+
+invariant the_reviewer_is_quicker_than_the_round {
+    assert REVIEWER_EVERY_SECONDS < 60;
+    assert REVIEWER_CONCURRENCY > 1;
+    assert REVIEW_ROW_SECONDS >= 120;
+    // a stuck row is still seen several times an hour, so one that starts to read is not lost for long
+    assert WAIT_REVISIT_CAP_SECONDS <= 3600;
+    assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS;
+}
+
+invariant the_reviewer_can_outrun_the_measured_finish_rate {
+    // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets
+    // at least the slots that load needs, and the ceiling is not what stops it
+    assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58);
+    assert REVIEWER_CEILING >= slots_needed(58);
+    assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY;
+    // the fixed bound alone could not, which is why it is only the fallback
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours
+    assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0;
+    // FR-001: 0 only when nothing waits
+    assert reviewer_concurrency(0, 0, 0, 0, 0) == 0;
+    assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/cards.json b/apps/website/public/learn/cards.json index 73ec07b395..d09ece2dd2 100644 --- a/apps/website/public/learn/cards.json +++ b/apps/website/public/learn/cards.json @@ -2,17 +2,22 @@ "learn/": "4d54cc27d150bfa3cdffeb226bb8ab2ca123ce9e53e07b6f413789a1cc1b870b", "learn/a-bar-detector/": "5a9f708e48318bfc36529c8f005b873f4f64a725b1f15c17f146d473a5c0a956", "learn/a-bend-at-zero/": "304f35f236649e02ffee68b93083557cc71795ffe38d65e4b618a5cd49d143f4", + "learn/a-boundary-of-real-files/": "2f8bdbdd2143accfb8f4022e1e16fc8bc82aedfe6f33e0eb530819f9fabbd2a4", "learn/a-build-receipt/": "a9f6033644a5c80be2d2502b7dd787b44179c68d6a8782d2f072b778a619b3d3", "learn/a-correlator-finds-the-frame/": "b161b3792e14fee0c3f1ff596f3991d4b36752f1a66965fdc0c075019fc4fee0", + "learn/a-deploy-mid-flight/": "82d33bd0ce17876455a1fc938aad896fd0c769882245255d5bf49246f34dbda8", "learn/a-dot-product-in-wires/": "b8c1bed3ff617584cb030d8d6aa8d07b05e283a9c73593e004cbec1279ec328b", "learn/a-float-cut-by-phi/": "9b6abd1d9c6730834e421ab2f561895d32fd8ff4bf0639230475658158ab5e0b", + "learn/a-key-rests/": "da42b6c8732e23b0b6ae816cb091a3a32e9acebba89463b2bb29e91bbb2e43b3", "learn/a-loss-in-bits/": "a421c6068ae82704dfd29716373b66c06ea0e02a17506714cfaf5f8fa53a37ba", "learn/a-model-on-the-board/": "b9365e72b52c80e6aa910619129ab8c27e852be816b72502e6601176e62a6b83", "learn/a-neuron-in-chunks/": "a80bbd72c568fc1e67ec83194c971d0f826431931e6d5921f19856db016c43c9", "learn/a-pass-that-checked-nothing/": "ac621c2b8313caf55b562752930fea20b0b93f031e36c3ca808e118befd0ded7", "learn/a-recurrent-cell/": "b886d540ec56e97ee85d906d87db66274e5580ad14dbd16d681d9417e59a96be", + "learn/a-refused-key/": "26533617c1e0e8a532373ed90a2f480131192ca2ce2ffb529967c3529a865c06", "learn/a-sign-is-one-bit/": "8a5aed2915cf9bb4b3e08c0409bbb3188f11be9c612cef3611dcf1cde46771c0", "learn/a-small-program/": "fba5eaeea36f43fc0a72cfe956a83e332e3317820097b101e4f2fa4a3d019421", + "learn/a-tasks-life/": "d57ce0f491115666e70f67e6c8354ca59a53c2f0e07f9a4f9d6d31c8348c8731", "learn/a-ternary-alu/": "d3c7d028d7b7cee31b580287ad6dd911c2f2de637e95de0efafe0bade69ae087", "learn/a-test-block/": "aa77c8603e32ff654062e3c958d97c3be30a28db32cdf02ad06aa4633f25e7b8", "learn/a-tiny-layer/": "8876f29ac162b6a4056ef502b7f9df8d82c3db7e904a8de156f32739f7e1b582", @@ -21,6 +26,7 @@ "learn/adam/": "8dd0d18341e62cd91d2025733d2682c10953bd7b8a54dd8682696e3f3cd91fcf", "learn/add-it-up-every-clock/": "9db8a04a5a0850939e6ea7f4f04ce478deeeba842b9ef818d42daca563fc8429", "learn/address-decode/": "4b7a49117662ffbd5cfbfb3948b4cd5147f03db14d243842f24259f7b770acbe", + "learn/aging-a-label/": "45a56a1b4082cb5d49027c806b19a5d17f821c426825db816c8d9cdd20e8e98f", "learn/ai-numbers/": "d9f22ce61d2271479c53486b027260f1c4713f13dc09d0acbdb20446d40d0f3a", "learn/an-encoder-block/": "18f9e0dcc364af47a9eea1f34e7585bcf1a21784a8930da4c51c7f72e997ce6b", "learn/an-honest-scoreboard/": "f782a3681c0ebc3b23fed57524c87b069c354c69c55f9dd6c4b7139b188096e2", @@ -30,6 +36,7 @@ "learn/arrays/": "672a72dbc0dbd6f2369a486163c6bc344543b913524c03fcac74f71b07c033d5", "learn/assertions/": "b769da059ed61e29a375f4b745144fef3177e80c356cdc2563b95dc98a0a7950", "learn/attention/": "c2b0c62c4c3e959d2d2e07028a9e65d53b7488f4178b16706696a82550143ae6", + "learn/back-off/": "22ce971ab6174e741f0c3ffa470502dead26a268fda9628beafda8110b8155a4", "learn/back-to-three-values/": "396c029632556c52a4ff5f3263e44c8a476d912b2652153f489c090f03a65f21", "learn/backpropagation/": "b7d1d781f541cb03e5adcdc522f8eb36f72f9210b1b151ac247ef59ccd5dcc8b", "learn/balanced-ternary/": "5e12a2e4d2973cb19f2923d1ef7680f40af095f29a2b19591781709ad3da175b", @@ -78,6 +85,7 @@ "learn/elu/": "33a824f89b3ad0074669ae9e7013f801c382852880ccd19da6af8d08dff348fe", "learn/embeddings/": "d50dc0e7d8074652c8cda96606c2e3c8ea1059fed52259b5828f2a30cce24e9c", "learn/end-to-end-scenarios/": "c27e513669976ea8d383d0684491c4b8e0e51ae7944ba00d8c9065e7c337c576", + "learn/enough-reviewers/": "0823e216db34f96dca1f9ebf5ba26c2ec5f6668526a153c4c2695778fd38b82b", "learn/enums/": "b24db7fe9adc1c0a984b07382e2ed7d98d876a46aa8306f48319f3e78c522462", "learn/everything-in-one-bench/": "61894d75448dcf97c154dba7838861da4fba39878a7e6435b4d3bfce3870510a", "learn/expressions/": "00ff8a5285eb3299566b450d2a944fda2e8b14853cb348e3b68cdef95824699f", @@ -131,6 +139,7 @@ "learn/how-much-fits/": "304e2785f886071ace463926a0402b10fc08d1fb5154e60b72263adfa769a0bf", "learn/if-and-else/": "1b1321a98895aa452db0a613a085f8c518467076ca15ee9835e69cb3cd55fe8f", "learn/induction/": "7d6ec297f7c7815991cd2e9013ce3fcc1b26caf0607b1e6a04f2600aca213750", + "learn/inherited-red/": "c8cf9042efa5c68e82b05efa76a275ccd1bbd133b77b995911454c81397e94e6", "learn/invariants/": "47eda6dceaf38ac2c08ad9375af84c18d9992dd48ac6707f49aaed6aead0db00", "learn/io-timing/": "c9d168580e39e3d7773114d136680fb8153f8fc17d0a00791b2025fe6695ed5b", "learn/jtag-asks-the-chip/": "8e9f0ea737a56a88be3f77f2a5bbd17f27ba807203524a41ef8958458e244e7a", @@ -159,11 +168,13 @@ "learn/multiply-then-add/": "d6a0407858df178e9de0ccc24cded3864ac19f8aaa796466913a9f3713bf3fc7", "learn/negate-twice/": "d8723c28c60ec0e1b4bdc5255af07d23204acea7a126caa544c51aabc7153fed", "learn/neural-networks/": "7855f1ffad9744a591df9c7eeb6af58949b7a7505e7956c307fe450e79fc4043", + "learn/no-credit-for-keys/": "16327861d57ae08c85ca36a7bd182131487f2b0e5330fda6f60ee765e2dad7b6", "learn/one-edge-one-world/": "5c3e2b05491bd8ac0f6f829324c830f0c3e7c44f7ba83af1a85b25645a3bc395", "learn/one-lut-up-close/": "166f3d723e87272cc8dfa905eec2ff86dc16c2fdef28c6c15988a6925aa6bd65", "learn/one-outlier/": "21a1cab7337d9d7c8481a6b31ea0b3e1dc3647bb8e16589f6f5b7a3c1eb291af", "learn/one-scale-per-block/": "6f19f66e43e7e7bfefe1a05b99032ae1a4877ff74b3ee78c56fb3ad61eb6c327", "learn/one-step-downhill/": "060b45a1ababf4d56f640e60d600dd955edf6559f8f8459891ecd604c513c837", + "learn/one-task-three-kinds/": "ceb4412abed9616e7f17e0f575cbb3e63f752126b9453267adf365d89cc045a2", "learn/one-ternary-neuron/": "9f91b48b09e93b6a1affb43597396e046d170b9ae9154853ab241154480c9b70", "learn/opposite-signs-subtract/": "2a067cd8add2ffd486c3bceca9dde5718d559ea05e2c98da89d272afba4746b4", "learn/patches-of-a-picture/": "7d61c57ac9aced7e6054c6ab5f71066bd61f1aaa8a02a1a375484577e9cb01ff", @@ -186,6 +197,7 @@ "learn/pub-or-private/": "a556551e39a7d31cd2db6a76eb55baee0f702f9a8f16db74d5fdc050dcfaec65", "learn/q-formats/": "bb9597c92651aa132631814dbc6d0eff72067d1bc1777b16d3c67346ae4ba9f7", "learn/quantization-noise/": "6e9cad12c183b8afe4cfcf5593c4e431a41ec87056a0fb60f66ca6780489f00b", + "learn/queen-and-hive/": "48eb90aeae2e981b3639ecdc3b9be4fb4f43906e2bd8d2d04df26d2cf460fdc2", "learn/questions-in-thirds/": "00842bca09d514998bcf058890cf794e7399f69d92203049e3f8b7f9c9dfc043", "learn/read-a-bitstream/": "6e929c80a2d5849da4a1e28f16dcb10fc833a2bb912e62b2e2d2c410c3c1707e", "learn/reading-errors/": "fd80e7b6b9483768bf331dc6706ca2e4321b4d3b204a33c45d5d7fe9b863e66e", @@ -193,6 +205,7 @@ "learn/relu/": "62de22babc94f73743ba95532b6ca8cb168d4c08e32e67e64d7ba43d5340e090", "learn/reset-fanout/": "45a31086006179bc14b727a0d771f0ac598a880bb39f00b654459f1dc52e87a3", "learn/reset-release/": "16583af4595233b0c6448670e8a94b8395738518c3c71899a474956a36cb4305", + "learn/review-first/": "c9a09e7af6de55f57196f36670c214adc8e4b18dd5117f79b3d302c1f874ff01", "learn/rgmii-timing/": "0a35740b109c3a3f66fe201d5bb2f061474a7225768c325bca396e5366c9c9d2", "learn/ripple-carry/": "71bc88d6b49ed41455bf17b8461e4aa28da6f813b17d6a9106844c684a2a5473", "learn/rmsprop/": "0d02c6bd58a357f575de0ff6cbfb950e388815f61c63a4cfc2ab523bd6bb62fd", @@ -207,6 +220,7 @@ "learn/setup-hold-window/": "0c7d7fedd13e5dd7b3043c8b3e44e7959e52bd94f9f871472644bc82438467f1", "learn/seven-backends-and-t27b/": "0c5e3bbcc8dde3484b604c27d7095ef330d69a2dadfdf47991a804928ab756cc", "learn/seven-backends/": "763e34cf9065f98c3c57289928de631dbfdab77073dd68ed1722adae85fcd26a", + "learn/shaping-an-issue/": "ec069dcbe3ea5fdcb7d5cf96651ab552d42ab7ad80a99292ebd6d7d4a8b37bee", "learn/shift-and-add/": "0fc9d1dc49e6aa7161cc37845eb83f35362705154a2a73d3c3a8f5808650672e", "learn/simulator-internals/": "edbc8a0f5538016c7b6a638296a2dddf2032aaf7ce2aacda891152ce313ef938", "learn/skew-and-insertion/": "a6baf709726e888f1066cdf6785a2b4ff2c0bd402d3609b48de1fe9decbd27ca", @@ -216,6 +230,7 @@ "learn/stages-of-t27c/": "b5318e6891271ce79cf978df0f986b01c85a9ba3bbaf6d17293175dae43131e9", "learn/state-machines/": "dc16422800c76c2e886f480416680232f10e0d074985ab49016c5e002d499804", "learn/status-and-fifos/": "72b95ca01512ed92246cf00f5f568b372e5e077c519b7153a787f5c69012e5f6", + "learn/stop-and-mute/": "6823ee85106ef1a7070a09e9cf166e244733d2a5e63df35ce48c743e16864021", "learn/strobes-and-waits/": "d782ed51a0c8393f02152ad476a4c2293f2076494f0266f18ea0d22dc33e0336", "learn/structs/": "12f9923befd6890498651ff8a7fb0cdf55f7bab8ceddb55872d83d891ab333f5", "learn/sum-and-carry/": "116114ab7cc7ce0c5b904908b524f2f2565c058e38978a21b3f9b1cd0760ad03", @@ -234,25 +249,37 @@ "learn/the-butterfly/": "25bed4bd16b6db4f3e6b6c6ca106a66d4aeea4f05c3d0965351dfe9968c1a826", "learn/the-carry-chain/": "6ca5b33263af99e956c83c7981fe6713d50fbf7adaa65c8983dfeaa6afa60001", "learn/the-cic-filter/": "e9096d9bc30bd1f30960b0b8735cbcd7676ed9543e4837696e26941de74b7c39", + "learn/the-comb-answers/": "06cf81743eef149fc132af4ff45cdb4ee44f464ab95bc8c196421cbef33c3236", "learn/the-dsp48e1-slice/": "f7f31aea3a13927f5a7edd06281ffc3c1979b41f6932aa9e45dd6252171eb3e1", "learn/the-frame/": "f8a52f994c8f6d9a68df9867b88d0d28705301fc214c761876b46b67abc9d3ce", "learn/the-huber-loss/": "b21aa0f2c51fa2f334f79e60e20736ff6f11bbf227f4613c52694840dd2d0e45", "learn/the-immediate/": "50d7bf9de6d8360d5f2ba4d1bf07feab8e9e0ed753019a18d33117ce239c8e1c", "learn/the-instruction-word/": "14831935e16dbb46bc03c53a2c0987d82169cfd48246a21975a2c42102fff49c", + "learn/the-merge-gate/": "29e24e84ccb5e379a02d102abf553a5955d1119dec814050169c81207860ea72", "learn/the-module-line/": "7725a10a6801a0af37813f12f60489cae8e40477132e92e01ea548c921906b54", "learn/the-moving-average/": "b8aafccc03df5ac669f07b870f3effe5af58f47aa5c1158f3ff2d34674aa2a4b", "learn/the-nco/": "ea99c6c66d499a85a98523ce027166473abffc1e48744b6cb90da871d52d3cc2", + "learn/the-next-key/": "72249fc1013469d61a8d87d42cd40416b334b35a988e8a895d5cd593d7c99b85", + "learn/the-night-watch/": "bf76f1b3bb171c6d575d0093283d63c24b2eff78371fc4f5297c65792bfe2a0f", "learn/the-opcode-map/": "3e2cb39a5bb0d97202e07342de00a61cb2e68e005f3891519d60a3d1d7a42001", "learn/the-packet-bridge/": "8d67ce0e85853b41145108f3a1a5a011a4c64db7531ce8dbec6444db09c90e4c", "learn/the-prescaler-ladder/": "c302a7136b59522064c97851b5b548d3cfbb50a0a3acdd1f9846ba2f0c263bb6", + "learn/the-public-board/": "2e9e212c1f00277dc4a9a8f46b2a73c82cc6f32d366349e76df3f4c3e34d84a8", + "learn/the-queens-move/": "77397611ed5a01a993eb7360db44092ab639c03818f88c78571772a442b19632", + "learn/the-queue/": "e0a6e567ac85e7ed61f1af56536083fd41eb0d65357160f8b0fe6991904d30a4", + "learn/the-review-jam/": "01072af59aafe6a9dba6e3fb03ccdd64b4912aab6f86cc236f18c5c2ddc874d1", + "learn/the-ship-flies-home/": "c55f4cc2d064296aa6226bf4d34305c0d5661f69b213aa7888d269a18811c59d", "learn/the-sigmoid/": "5904aa9031189bdefbf546505a5aa6f8f03f56cc87b39a96278a89c071836f84", + "learn/three-bots-deep/": "52368e237c49adfd3c6ad5370f3712ae9f26519be0f5566d3391983ac97123e4", "learn/three-way-agreement/": "0e6fb44f8c6c2f5de5f2a29546ec0d9e075b08bfa765c92d09306d2fc6c538ff", "learn/three-way-jump/": "409f5af46290fc7a48563787c90638949281b5a1728af9e4474f05ff442e590c", "learn/times-a-trit/": "2517a84f8c4cb482b873580dcd6d5577f3dac99f2afd327be6ae11129d80594c", "learn/timing-slack/": "da7637627c70011731517c34ddc8cc14f42878b45b29b65f613a115e454fef1a", + "learn/tokens-that-burn/": "ab6e7bcce6dc8ce621d5e09d8597eafca74397c56972330118dd699b5a53032d", "learn/tri-commands/": "2bfa52f1cf2bec6d70f90dce796b7ec83ca0edd72a2acdc489ca77e8001c71eb", "learn/trits/": "14170e1a24e80d17658d8b6a1fb758602ae86a858c42793c87b53d31892f3d18", "learn/true-false-and-text/": "a8302d71500c7264aa2cfab2823eb764d736c49726f7299c30bc9b5111f8785f", + "learn/twice-the-cost/": "6bfe991eaded27bf5570d72cb8dce12228c3dc6a2bb9da4b9b2143a747991b41", "learn/twiddles-and-bit-reversal/": "7795db9e1795337aef744c9c32a55c1f0a618733b92c1e7e07f938a0b8f9465e", "learn/two-bits-per-trit/": "d74ff7a386a7fe2b050ec60e0b83c6b52ddd3735511c5a6770ff5ae353709eac", "learn/two-flop-sync/": "c3959e859f480b7370878aff377418e9722b32ea22965529830aeb6ad0a4f08d", @@ -271,6 +298,7 @@ "learn/where-the-luts-go/": "8b235e1936c19d9d4235006d1dfa27a4bb56a2b1a969fb2d91dcf390c1ddd6f2", "learn/which-chip/": "2b5d5bdae258a5a588616770b0d860d479e76afe782028ad904c6c9964854863", "learn/who-holds-the-io/": "939445af92fafb2b4096199de09414cdc94ede53817856cbdb6416a3db9adeb8", + "learn/who-is-called/": "c9b57042dc713ac3741fedc4e5830afa30c53aacfcbad679a78c3deab2f0b072", "learn/whole-numbers/": "90ddc9a9b10c8bc1980a1b2a183b473d9fb950b28aa3f983248fde085d42ca3e", "learn/why-base-three/": "5e7f3aeb9b8299e9fe71d1cd38f3a9d684b3a662d9a07d1c1a8a9ddec932df25", "learn/why-binary-fails/": "f7e9bbb3d5f7acb4c45e57e4bfa9b5c421b9f0e0b142ef75159e2c29dd653400", @@ -282,17 +310,22 @@ "ru/learn/": "a9995b9648745b8be6063618ec0103bea67dafbb850ce40d7ac87fab1f9f1d91", "ru/learn/a-bar-detector/": "5d205d40588feda96bd5f8464615857c7b8b11f80f4bd32ab9820ff78beef551", "ru/learn/a-bend-at-zero/": "cfd85d458fd00c986dbd0b1dfbb87f9d78b4692f9ff69e672e2f6fc5e4c2bd6e", + "ru/learn/a-boundary-of-real-files/": "d78765672202fe83a777a995182b357fd0dfb4061b1ae75be7407c0cec83a14c", "ru/learn/a-build-receipt/": "5fd31459c1a61efbb7e893de7ed3826d0baeb3e2a2ca83bf0e07acb912e3939d", "ru/learn/a-correlator-finds-the-frame/": "1c37c50ee2662d7bf7292ae2af6cbfdec510597970349161071a00ec3ad08c3d", + "ru/learn/a-deploy-mid-flight/": "d80da132746d40a2b5d02fe230d043423a044b5fa8e8be56c441990a4edf32e6", "ru/learn/a-dot-product-in-wires/": "e623d894b5e9d556b714ea7067c6a322de2d8f70d5cfcba9b135fe7e2e9254b8", "ru/learn/a-float-cut-by-phi/": "be7a9f649e648df28ab9a6c98f86ecccff7f66cecd2b2776d20853fd7f89629f", + "ru/learn/a-key-rests/": "8d4f707d615418a8b99dd60e514f9df66d8a88d7f85a56426f8412cfbbaf260f", "ru/learn/a-loss-in-bits/": "4b064c3293a3be7767f59dfeba99ba2d5ffb8be53383291eb53cbd7cbb5efa80", "ru/learn/a-model-on-the-board/": "2285956627292be6c86d370ebee1ad16a5b07f62c8ebead82f96f490bd6b5bf9", "ru/learn/a-neuron-in-chunks/": "57e4dcdd5f09686a9cc7e2c19abbedc295dd5298e18073757e50a26abbbda461", "ru/learn/a-pass-that-checked-nothing/": "8ebe0c47613887dd80aa89f9727970c5953d360be26b70a0a166f7e8d2ae2df9", "ru/learn/a-recurrent-cell/": "d2633234b6b1ea462e2b619c7075e432f351cf996203954f48a47c79f16c9d1e", + "ru/learn/a-refused-key/": "0c19e73254bcaa4fe652bdbb7395ae0d85566eeee183959425891dc4c1e9820f", "ru/learn/a-sign-is-one-bit/": "aab0eefed96d2317d7a7cfb804a9d94fbd601946b1ce699f9cbdd601d26bfe51", "ru/learn/a-small-program/": "ad8c3ecea45c6f884776eab43bfd8669f3d7e50daadc2df7877aeec24d6d5f00", + "ru/learn/a-tasks-life/": "f082bbff6de3eb405394a69b619d92e288e98a213b667b82d46feacb0cd62b1f", "ru/learn/a-ternary-alu/": "b3a7bf73c1180a4ac31f815cee04e25fa6c3a6a0e7b021baca23bda6e5c82420", "ru/learn/a-test-block/": "e53ab31255bb2e1cccbc26aa35fadc89871c6cfd80caf5b1351237872beebb46", "ru/learn/a-tiny-layer/": "d27ccfcefe172a1a90e863f7482020a06115e96080e45c9807ed2be5999b718c", @@ -301,6 +334,7 @@ "ru/learn/adam/": "f562e3ee7b54b1f713f9e50b49b2506025f75cccdfe67bc3b179483e247dd8b4", "ru/learn/add-it-up-every-clock/": "289375b6b343a7454a2fbbb41d8d2b13b40c9901ccb18316a2df370c0111a8a8", "ru/learn/address-decode/": "08c58078171cfdbb20d55768c98efa29a58e869761dc4d5401a028f0d221a2a9", + "ru/learn/aging-a-label/": "014c7593f777b76868bf72d860e2b34e7f269a570496565b96527b70bdde0ea5", "ru/learn/ai-numbers/": "2c3abbe2f6527300151af1755677e763daed3f7af6a80af188f134c45120832f", "ru/learn/an-encoder-block/": "7882788e01d39206dd8d260ae048ec1308f0d6a9b93729f3797a26fe8f9bbd2f", "ru/learn/an-honest-scoreboard/": "a7a3612e27aedba4c0d67490481efd9d0fb7f4f67a72041d0d3db1ddf6e2adb2", @@ -310,6 +344,7 @@ "ru/learn/arrays/": "0e6bbb8781017b1073bb0eb3b8be8664d39295e27b71fcd9988ae5cb8b49723c", "ru/learn/assertions/": "ebf46984f1df8e3ce1b21b643137cc3a1becd01255fd3e0cb4d9c145c4bd3aee", "ru/learn/attention/": "d0c0658e4aef063126a96df015efb66af7f229482109c877a00d554add1afd50", + "ru/learn/back-off/": "9b5da081e28b5193956b5c67524d24e853c5a12f461cec64e9561c37334c2f42", "ru/learn/back-to-three-values/": "e228a4bfa47839abc0a2493e18c43cad1f2639cadebf10d983c1d43664b29580", "ru/learn/backpropagation/": "583e1264d2c4f9f37c4a81c366e39dc1ee3bab9cca2a636150431da3dbf3b89c", "ru/learn/balanced-ternary/": "248e4d5fd125f09776cc8c2fcd230a79d4162420f36009be768fed92a0177414", @@ -358,6 +393,7 @@ "ru/learn/elu/": "f53329f213a925d10d3448767dd01a966aec2a22a56b1f6359947cfb79a22cc5", "ru/learn/embeddings/": "6e1b5cdc0549e1dca8db68f44fd02e26ffc345a0c1a559ab1fd07f24ebe181aa", "ru/learn/end-to-end-scenarios/": "4637378e907d5de8cf6f6d9c81c11b1d9656b23503642bb67c156f08b84b02bf", + "ru/learn/enough-reviewers/": "075cefa61a5c96cd414464140e64fe0788415c77e81e8b9b607aad02830fe330", "ru/learn/enums/": "5587fcadd6a785468dd51151e352a68826c5e5d090e6da47b0a87a776ebd8c47", "ru/learn/everything-in-one-bench/": "5c9630c3f9ba333054364ef7d856bb3c07d02f7e7befa99d45448af30b4013bf", "ru/learn/expressions/": "beacff97524bb5317b17e64c38da09710ef5571b1078bfcd3b6ec27e4eee7329", @@ -411,6 +447,7 @@ "ru/learn/how-much-fits/": "f1651caa027f908485ae8b73ee470c082470c50ef18354b2d90981771bba2ee1", "ru/learn/if-and-else/": "e96c564b228831749334fe884cb942ac2c3e13e05a4b68efed9e061a5f3a95bb", "ru/learn/induction/": "e72c5224e2179d6fe0376ae646f2d0a85b1cbaa612654caeda4c944c2749484a", + "ru/learn/inherited-red/": "962eaeb31d25ff460b2777af3e54d79092b01125565b8b5fb17a9a1eedf2e003", "ru/learn/invariants/": "3c68c5ee8816024eb6b902b11ce824e6a35733e5375cdc8e83918818ffffd8f6", "ru/learn/io-timing/": "8855d6121a3a7b3a54436b4574df5e138225414a97bef36e7c1d94a5634c9131", "ru/learn/jtag-asks-the-chip/": "64f16cb0c154455de759fdb8a5b18098edef4bfd9bceb73395fa252591f64287", @@ -439,11 +476,13 @@ "ru/learn/multiply-then-add/": "61ebf2be71bb9bc7b1391a9ce5f410442dd84eaeb529d588a35ca51501a9edd2", "ru/learn/negate-twice/": "392a6c948a48c4ab72b2cf30d4e1378a95408671c288cdfd3cc4e76569a7ad6a", "ru/learn/neural-networks/": "e2c3701580205e96e14ab12193840f3d50c12e83578151df327a680e9f923f94", + "ru/learn/no-credit-for-keys/": "6ad5e601bc14a2c578b9cc04809e5d652aa3347a85faabf5d6884e1701b8576a", "ru/learn/one-edge-one-world/": "2259c1b021214040d6e338b1b806a2cd6220c651a45b8d2154afa534a31f478a", "ru/learn/one-lut-up-close/": "3f41d0df7387a37b49b04b2f443fa6777ed1bf02a72efab59ad537a31aa215f8", "ru/learn/one-outlier/": "c34f1bb2662cd52c783d6eaa53842331d3c24f6e6de7a2fb3f296650d4e0c463", "ru/learn/one-scale-per-block/": "3996dff8150a65875d22620cba95a70a8e5e29a4c2714cfaf5f370723018bdd5", "ru/learn/one-step-downhill/": "c2172182b46bb262978253fb942075fb0a35ef152f88112228957720efc4de27", + "ru/learn/one-task-three-kinds/": "6144060d1f38240647656e4852b4aed6ea477238d522b4e6c6ec80454909531b", "ru/learn/one-ternary-neuron/": "120cc517171191a4db21ea02a42604e03d7529ab9370b9e0ecc6a7c1b3d70e41", "ru/learn/opposite-signs-subtract/": "0a9a4ec69219b10e26620e9ef4c9f91834367ab7d65ab8fda5584880bb018622", "ru/learn/patches-of-a-picture/": "4b4b94a2cd72fd9689f4ea1ce717ad369b8a06408771f2532437e51ee0b90cdf", @@ -466,6 +505,7 @@ "ru/learn/pub-or-private/": "90dc28ccbcd693817876da6702a39d83d195bc379bc4fdd7a334f0f0d2fc9cd4", "ru/learn/q-formats/": "17f3e29df1567b81e1ebe4383e328157f874301b7d84897d65701ae70887b2c2", "ru/learn/quantization-noise/": "c42fb391df5346fd3db4f28a3a3af26e6687161f7de05b08d19d87a2f54bec0d", + "ru/learn/queen-and-hive/": "56d37b7e312ee73d8b011235408ed7c56527a945f543f2ca73a97daf9c9667f8", "ru/learn/questions-in-thirds/": "5f116646bdb6151435a38e20c632ce0355aaca3b2b8f6f9910056279ee18367b", "ru/learn/read-a-bitstream/": "dd8e56af7d6109c70d3df54587a16457ff42b8b463a4be851c0c8544b1b5d5de", "ru/learn/reading-errors/": "08ad4f65e5ebb8133be23f8aa5aceaaebcb3c86c540f54b77935d140f1201712", @@ -473,6 +513,7 @@ "ru/learn/relu/": "f499830dda1d7249191dcf9b7f90f60bd7b7d0a19dde57cec9b118446966ee85", "ru/learn/reset-fanout/": "e78a12590614ec49df0520220cd5ecab610e889381403317c45c19394a164839", "ru/learn/reset-release/": "7eb4b8202ea9143644bb7e00257fa13986827c3e1475f23e96e5c441647eac00", + "ru/learn/review-first/": "60d11543d697e0b860d4fa5b9b184cc24d123635a79647e4b2e3bf10f5d152b3", "ru/learn/rgmii-timing/": "c5da1b914f3dacd9aa298018073cc64235ca5bcc21b4e55e48c533a1c43f3069", "ru/learn/ripple-carry/": "69c2c82cd29b4514dad063f586e20478028dd0850f089d23bff50a7464fe0dc4", "ru/learn/rmsprop/": "36f402b801339ff3058c74473457080f4a8816e0f517dd4d3901662cb77cf42a", @@ -487,6 +528,7 @@ "ru/learn/setup-hold-window/": "c4e50f3431fadcc2d123acf5caec686d4e6ec26d637a26edc27fec50736603d7", "ru/learn/seven-backends-and-t27b/": "18800cff6161005f4a65b270e72416d0a4a20442c9f306be9e5f6ffe248caaed", "ru/learn/seven-backends/": "ec337af2485ab17106d8ce74fb4ca14f4660896bae49cfc1ade4fc66e69e4644", + "ru/learn/shaping-an-issue/": "c2f3c6e0a66ad32ca52d455bbf040cfb67e42d02b2973065ca707ca10c927481", "ru/learn/shift-and-add/": "21b8b125f392df329f5da2c35025958705c3cb26b30fa4426c280ef4ecaf5b7d", "ru/learn/simulator-internals/": "eb52f4fb43d5013993fc65acbcddcc54c8c487ee7d3ac60cf0fae516bec1d5fe", "ru/learn/skew-and-insertion/": "875431550bd473591ca92fef039b21dfb8558a1fb642286f734fa5d3921ef074", @@ -496,6 +538,7 @@ "ru/learn/stages-of-t27c/": "38cd775b193c470a268a9338560ea8b2ead25bdce09030c8a85276ea7a5da66d", "ru/learn/state-machines/": "21427ad4b1651d5886de45cdad9e08b0591f1ff2a88419db94f85617309c7409", "ru/learn/status-and-fifos/": "97aa883d53389e65b258dcad40454a7cef44c4e8635e9caa12093bf28ba34007", + "ru/learn/stop-and-mute/": "56b1369f75eb5b2581e00a005d50f561c5e822bf9d40996b9f7aa3c047ef85cc", "ru/learn/strobes-and-waits/": "ee5230958b3bbc959f56e26657375e3abb24120d58edc6996b7752ebd8b721f6", "ru/learn/structs/": "dba08039918a7755dca4fc21059820cc39d7bed801b60971bef428648bc0e339", "ru/learn/sum-and-carry/": "5d5314c50224b0492c133b289adfabcbb366383d576685aae34f84df9c60f839", @@ -514,25 +557,37 @@ "ru/learn/the-butterfly/": "77486f716a1213c346d69a125cdaae01304cea0b9697650bb587ae1252ab4089", "ru/learn/the-carry-chain/": "c4009c18f64855044372522a011415266954dba9913c4c6a956321379abb25c9", "ru/learn/the-cic-filter/": "2fa2d18d844d01347cee42df32e370c411892213414983c3fb1b12b519dcb1c4", + "ru/learn/the-comb-answers/": "59566f6b0ca69cd168af24290e4c25f4dbdb8391fcced71020119be678416580", "ru/learn/the-dsp48e1-slice/": "bed22e7faac052e1e867e04d473e78818ba6d06169f121ed18f7e40a9e6f3b4a", "ru/learn/the-frame/": "67b1bea598485ead866b58b16931b727c495211edf0311742c9c60668499b68f", "ru/learn/the-huber-loss/": "acf0722b9e86dbb95c06097c7c4574f5d6e89795d281aca1722ea3113c45d0aa", "ru/learn/the-immediate/": "8c46a039bbba3c821e23cf3a2a8d2e5d621bf1b82a094a669eaaf4d2aa122005", "ru/learn/the-instruction-word/": "1c505c99338c17d12117db730f1cc372f40139e6c4ea35c0cd570a33edb0048c", + "ru/learn/the-merge-gate/": "7570b74db322a63216b3e48a54554a267b532f1ed692dd5f5c7e2b71bf53fdc9", "ru/learn/the-module-line/": "64e925ce7f71cbd03711755222aad715d071a52a839188b0ec12573039bd8487", "ru/learn/the-moving-average/": "bb33984b6b07bd4a466aa5d47b4a44a2965604fc62b049dd4d6d074c59577f3c", "ru/learn/the-nco/": "c8e952e10bf8e650016cc5f63c44429955489ddbfc7d1a49f9754fdf91fb405a", + "ru/learn/the-next-key/": "89dc68140aac80ea277b39204d499c2e6561a32a8b37c6224c2c63d681f66093", + "ru/learn/the-night-watch/": "7cf93bcf97149ea453113766a6fd3c814af4f60144b696871908492b81aa5849", "ru/learn/the-opcode-map/": "5e1e16352052b3ea4155fdbc24b9736bba0457f77010399087ca0006b492b78b", "ru/learn/the-packet-bridge/": "42db292451ff065051853a1fe6e5b76cac209439865b0c04ca92d2aeed63a328", "ru/learn/the-prescaler-ladder/": "24f40c27913d09799de1f355979afb3c7ce33077c623afab1047ee5fce33e98b", + "ru/learn/the-public-board/": "718ca4bb3e8c4ad8c5fbb37aa677ac0b6e6ea778c773abf0d8597997342d60ca", + "ru/learn/the-queens-move/": "d5af4501bdf01f8c5e89981277aac3850da77db7ca4805d9744fa94d7d3f97b6", + "ru/learn/the-queue/": "56aa67d32d3d13c73c60943dfa8777f83ea0feeec061bc47df47d6b1a4680752", + "ru/learn/the-review-jam/": "78aafe8afe04a23bebcfee879e31188dbd0e3dfe0b52e341851a6ca644db3a33", + "ru/learn/the-ship-flies-home/": "889f4c45b764b0284a4690f8bb4f74cdb187f7736ab122570f479f7b4fefbed7", "ru/learn/the-sigmoid/": "fbbad14c4eab03da0e1ed1b0246852f0506eef804c15a67598a5796f0467b563", + "ru/learn/three-bots-deep/": "09535d0da54af111309f5b69a45d0420843d9020de0a063770a4a56489b60f66", "ru/learn/three-way-agreement/": "bebf6245e54bf8c7bdfd9d0bd4333c1697f8e584edef789e25d94e4c58502928", "ru/learn/three-way-jump/": "f62cb714d30730385eb361748e15285092ef19461941777ae929aea1e432783f", "ru/learn/times-a-trit/": "a26840e4f613ec942520b27fc81863e0f3a88493b37cb653b395d51ae67a7a0c", "ru/learn/timing-slack/": "e033691bb4ccde44905ccb0a31d683b607eab84d193e2f6c668595c55a1e8fa8", + "ru/learn/tokens-that-burn/": "d98cbb39b43e753e66bad89c3e9ff5342247eb704986a48ebbaa4a6113ca744d", "ru/learn/tri-commands/": "fe7eb672f8a9d85aa161e60f7da56ea03b6c9db7457e60b85905ee1981c4833c", "ru/learn/trits/": "4c50162d2abd2b855194a30626d5f42e8dbff2c484e95bba39977c5bba7a5349", "ru/learn/true-false-and-text/": "1db693d3f318c2ae7568a64dc6ac764df5293b889f5122e5dd95990feb462dbf", + "ru/learn/twice-the-cost/": "5e2820fe8adf118f3305d89e2436e27f17062f7397661dfd54bde4467b64a3eb", "ru/learn/twiddles-and-bit-reversal/": "641af4c2d6dd6c4aebde76edb7d97645a400b2f60c33e5d1d852c3615e518ce9", "ru/learn/two-bits-per-trit/": "61c34b29ec106f049beaf89b0ae028294b63fa259f258661c430ec4a222fb5f2", "ru/learn/two-flop-sync/": "3214814cce95801bd40f6f68fe332b3a845c84380e7bcfbdf4181d406590b3db", @@ -551,6 +606,7 @@ "ru/learn/where-the-luts-go/": "6e266040a131ced97691730967ca3196117bde914b0797e603239de604bdba92", "ru/learn/which-chip/": "f4d99ed56b68b100bace89f983367c60046201d156741427d951ec707d1824f1", "ru/learn/who-holds-the-io/": "46022c369c971afd34efacf7883302666fbab77ba6ce63c368393865bfa47a58", + "ru/learn/who-is-called/": "55c925b0bffaa7aac3c75af3cd9c90d17bedd6a41c6a50d117d759bbc7ffc36a", "ru/learn/whole-numbers/": "d2018dad26722dedb12d950254909cd809f78f715bcffdca32513169503da3fa", "ru/learn/why-base-three/": "8fefbde4906af976f882c83074afec1368a7d471df39bde82ef2986b8d8f3e68", "ru/learn/why-binary-fails/": "8a11993f7d149f15d30238bbc73caa2ff7d93a62f237ae104d221577518cd16c", diff --git a/apps/website/public/learn/enough-reviewers/card.png b/apps/website/public/learn/enough-reviewers/card.png new file mode 100644 index 0000000000..304a01e69b Binary files /dev/null and b/apps/website/public/learn/enough-reviewers/card.png differ diff --git a/apps/website/public/learn/enough-reviewers/index.html b/apps/website/public/learn/enough-reviewers/index.html new file mode 100644 index 0000000000..50396a40a0 --- /dev/null +++ b/apps/website/public/learn/enough-reviewers/index.html @@ -0,0 +1,390 @@ + + + + + + +Enough reviewers: the Queen and the hive course, lesson 10 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Enough reviewers

+

You will learn

How many reviews must run at once for the reviewer to keep up with the bees.

+

A review may take REVIEW_ROW_SECONDS, 300 s, so one slot clears at most 12 rows an hour. On 2026-10-09 bees finished about 58 an hour while 111 rows waited, and the fixed REVIEWER_CONCURRENCY of 4 tops out at 48. slots_needed(58) is 5. reviewer_concurrency takes the keys, their lanes and the free memory, never goes above 16 and never below 1 while anything waits, and keeps_up compares it with the finish rate.

+

Try it

Find the fewest keys of 1 lane that keep up with 58 an hour; then cut the memory to 1000 MB.

+

Open the interactive lesson →

+
How many reviewer bees: keeping up with the finish rate
How many reviewer bees: keeping up with the finish rate ↗
+

Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work.

+

specs/queen/reviewer.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time
+; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the
+; live Queen (gHashTag/BrowserOS trios/agent-server):
+; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91);
+; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after
+;   another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to
+;   2 minutes -- in practice about one review a round;
+; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min).
+; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each
+; on its own lane, one row never twice at once, and every waiting row visited in turn. The round
+; then reviews nothing: two sweeps over one row would race on its verdict.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewer;
+
+/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy
+/// costs one query.
+pub const REVIEWER_EVERY_SECONDS : u32 = 10;
+
+/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the
+/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds).
+/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays
+/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour.
+pub const REVIEWER_CONCURRENCY : u32 = 4;
+
+/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and
+/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`).
+pub const REVIEWER_CEILING : u32 = 16;
+
+/// Seconds in an hour; finish rates are measured per hour.
+pub const SECONDS_PER_HOUR : u32 = 3600;
+
+/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer
+/// shares a 45 s budget with the round, because it no longer holds the round up.
+pub const REVIEW_ROW_SECONDS : u32 = 300;
+
+/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58
+/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS
+/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below
+/// the finish rate even if every review used its whole budget, and the backlog only grows.
+/// How many rows the reviewer may take at once, from what the host measures:
+/// - `keys` model keys, each with `lanes_per_key` lanes free for review;
+/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review);
+/// - never above REVIEWER_CEILING, never above the rows `waiting`;
+/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains.
+pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 {
+    if (waiting == 0) { return 0; }
+    var k : u32 = keys;
+    if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; }
+    var l : u32 = lanes_per_key;
+    if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; }
+    var bound : u32 = k * l;
+    if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; }
+    var by_memory : u32 = 1;
+    if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; }
+    if (by_memory < bound) { bound = by_memory; }
+    if (waiting < bound) { bound = waiting; }
+    if (bound < 1) { return 1; }
+    return bound;
+}
+
+/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every
+/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600).
+/// Split by the hour so no u32 product overflows.
+pub fn slots_needed(finished_per_hour: u32) -> u32 {
+    let whole : u32 = finished_per_hour / SECONDS_PER_HOUR;
+    let part : u32 = finished_per_hour % SECONDS_PER_HOUR;
+    let part_seconds : u32 = part * REVIEW_ROW_SECONDS;
+    var extra : u32 = part_seconds / SECONDS_PER_HOUR;
+    if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; }
+    return whole * REVIEW_ROW_SECONDS + extra;
+}
+
+/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows.
+pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool {
+    return slots >= slots_needed(finished_per_hour);
+}
+
+/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but
+/// no more than the rows waiting.
+pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 {
+    if (in_flight >= bound) { return 0; }
+    let room : u32 = bound - in_flight;
+    if (waiting < room) { return waiting; }
+    return room;
+}
+
+/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed
+/// REVIEWER_CONCURRENCY, unchanged for its existing callers.
+pub fn review_slots(in_flight: u32, waiting: u32) -> u32 {
+    return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting);
+}
+
+/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never
+/// visited before any it has. Without it, a row that is re-read and waits again (no review spent)
+/// keeps its place at the head and the reviewer circles the same few rows.
+pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool {
+    if (never_a and never_b == false) { return true; }
+    if (never_b and never_a == false) { return false; }
+    return seconds_since_a > seconds_since_b;
+}
+
+/// While the reviewer runs, the round reviews nothing.
+pub fn round_reviews(reviewer_running: bool) -> bool {
+    return reviewer_running == false;
+}
+
+/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen,
+/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could
+/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row
+/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned
+/// about 430 visits of the reviewer's slots.
+/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never
+/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that
+/// has started to read is visited at the normal pace again.
+pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30;
+pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900;
+
+pub fn revisit_after_seconds(waits: u32) -> u32 {
+    if (waits == 0) { return 0; }
+    var wait : u32 = WAIT_REVISIT_BASE_SECONDS;
+    var n : u32 = 1;
+    while (n < waits) {
+        if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+    return wait;
+}
+
+/// Whether a row may be taken now: one that never waited always may, and one that waited only after
+/// its revisit time has passed since the last visit.
+pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool {
+    return seconds_since_visit >= revisit_after_seconds(waits);
+}
+
+/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the
+/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait`
+/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only
+/// when the bee committed something, so a bee that committed nothing leaves no branch here, the
+/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed.
+/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free
+/// again after the retry floor.
+/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle
+/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit,
+/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty
+/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits,
+/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner`
+/// false) and a row whose bundle is stored are never decided here: their diff failed for some
+/// other reason, and that still decides nothing.
+pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10;
+
+pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool {
+    if (on_runner == false) { return false; }
+    if (bundle_stored) { return false; }
+    return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES;
+}
+
+test a_pass_fills_the_free_slots_with_waiting_rows {
+    assert review_slots(0, 0) == 0;
+    assert review_slots(0, 2) == 2;
+    assert review_slots(0, 100) == REVIEWER_CONCURRENCY;
+    assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1;
+    assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1;
+    assert review_slots(REVIEWER_CONCURRENCY, 100) == 0;
+    assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0;
+}
+
+test the_bound_follows_lanes_memory_and_backlog {
+    // nothing waits: no slot, whatever the host has
+    assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0;
+    // lanes bound: 2 keys x 3 lanes
+    assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6;
+    // memory bound: 1000 MB at 300 MB a review
+    assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3;
+    // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free
+    assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16;
+    assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING;
+    // never above the backlog
+    assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2;
+    // at least one while anything waits, even with no lane or memory measured
+    assert reviewer_concurrency(0, 0, 0, 0, 1) == 1;
+    assert reviewer_concurrency(10, 10, 100, 300, 111) == 1;
+    assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1;
+    assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1;
+}
+
+test the_measured_load_needs_five_and_the_bound_admits_them {
+    // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5
+    assert slots_needed(58) == 5;
+    assert slots_needed(0) == 0;
+    assert slots_needed(12) == 1;
+    assert slots_needed(13) == 2;
+    assert slots_needed(3600) == REVIEW_ROW_SECONDS;
+    assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1;
+    assert slots_needed(4294967295) == 357913942;
+    // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true;
+    assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16;
+    assert keeps_up(4, 48) == true;
+    assert keeps_up(4, 49) == false;
+}
+
+test a_pass_fills_the_free_slots_against_the_computed_bound {
+    let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111);
+    assert review_slots_within(bound, 0, 111) == 16;
+    assert review_slots_within(bound, 11, 111) == 5;
+    assert review_slots_within(bound, 15, 3) == 1;
+    assert review_slots_within(bound, 16, 111) == 0;
+    assert review_slots_within(bound, 17, 111) == 0;
+    assert review_slots_within(bound, 0, 3) == 3;
+    assert review_slots_within(0, 0, 111) == 0;
+}
+
+test every_row_is_visited_in_turn {
+    // never visited goes first
+    assert visit_first(true, 0, false, 9999) == true;
+    assert visit_first(false, 9999, true, 0) == false;
+    // otherwise the one visited longest ago
+    assert visit_first(false, 300, false, 20) == true;
+    assert visit_first(false, 20, false, 300) == false;
+    assert visit_first(false, 20, false, 20) == false;
+    assert visit_first(true, 0, true, 0) == false;
+}
+
+test the_round_stands_aside_for_the_reviewer {
+    assert round_reviews(true) == false;
+    assert round_reviews(false) == true;
+}
+
+test a_row_that_keeps_waiting_is_visited_less_and_less {
+    assert revisit_after_seconds(0) == 0;
+    assert revisit_after_seconds(1) == 30;
+    assert revisit_after_seconds(2) == 60;
+    assert revisit_after_seconds(3) == 120;
+    assert revisit_after_seconds(5) == 480;
+    assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS;
+    // a row that never waited is always due; one that waited is due only once its time has passed
+    assert due_again(0, 0) == true;
+    assert due_again(1, 29) == false;
+    assert due_again(1, 30) == true;
+    assert due_again(6, 899) == false;
+    assert due_again(6, 900) == true;
+}
+
+test no_bundle_is_empty_for_a_runner_row_past_the_grace {
+    // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true;
+    // the six hours the frozen-wait valve would otherwise have held it
+    assert no_bundle_is_empty(true, false, 360) == true;
+    assert no_bundle_is_empty(true, false, 4294967295) == true;
+}
+
+test no_bundle_is_empty_waits_inside_the_grace {
+    // just finished: the runner may not have stored its bundle yet
+    assert no_bundle_is_empty(true, false, 0) == false;
+    // a bundle 40 s late is still awaited, and judged on its branch when it arrives
+    assert no_bundle_is_empty(true, false, 1) == false;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false;
+}
+
+test no_bundle_is_empty_leaves_a_container_row_alone {
+    // a bee that ran here has no bundle by design; its diff failing decides nothing
+    assert no_bundle_is_empty(false, false, 0) == false;
+    assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(false, false, 4294967295) == false;
+    assert no_bundle_is_empty(false, true, 4294967295) == false;
+}
+
+test no_bundle_is_empty_leaves_a_stored_bundle_alone {
+    // the bundle is there, so the diff failed for another reason: still a wait
+    assert no_bundle_is_empty(true, true, 0) == false;
+    assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(true, true, 4294967295) == false;
+}
+
+invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour {
+    // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit
+    assert NO_BUNDLE_GRACE_MINUTES > 1;
+    assert no_bundle_is_empty(true, false, 1) == false;
+    // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit
+    // that judges it comes within the hour of its finish, not after the six-hour valve
+    assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR;
+}
+
+invariant the_reviewer_is_quicker_than_the_round {
+    assert REVIEWER_EVERY_SECONDS < 60;
+    assert REVIEWER_CONCURRENCY > 1;
+    assert REVIEW_ROW_SECONDS >= 120;
+    // a stuck row is still seen several times an hour, so one that starts to read is not lost for long
+    assert WAIT_REVISIT_CAP_SECONDS <= 3600;
+    assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS;
+}
+
+invariant the_reviewer_can_outrun_the_measured_finish_rate {
+    // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets
+    // at least the slots that load needs, and the ceiling is not what stops it
+    assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58);
+    assert REVIEWER_CEILING >= slots_needed(58);
+    assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY;
+    // the fixed bound alone could not, which is why it is only the fallback
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours
+    assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0;
+    // FR-001: 0 only when nothing waits
+    assert reviewer_concurrency(0, 0, 0, 0, 0) == 0;
+    assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/index.html b/apps/website/public/learn/index.html index 68a881f968..016a593112 100644 --- a/apps/website/public/learn/index.html +++ b/apps/website/public/learn/index.html @@ -63,7 +63,7 @@

t27 basics: the language in 27 lessons

Before the FPGA course: what a t27 spec is, how the compiler reads it, and what it gives back. Every lesson opens one table or diagram drawn from its own spec and one spec you can run in your browser.

Course 0, 27 lessons, one infographic and one t27 spec each: a module, constants and types, functions and tests, then the compiler's stages and the 7 languages it writes. It comes before the FPGA course.

Open the interactive course →

-
  1. t27 basics: the language in 27 lessons
  2. From zero to a chip: FPGA programming with t27
  3. AI numbers with t27: from number formats to a ternary network
  4. GoldenFloat with t27: one rule, seventeen float widths
  5. Verifying hardware with t27: testbenches, waveforms, formal, mutation
  6. Clocking, resets and clock-domain crossings with t27
  7. Buses and peripherals with t27: UART, SPI, APB, AXI4, Ethernet
  8. The ternary machine: from a trit to a running program
  9. Arithmetic and DSP on the FPGA: from the carry chain to a filter
  10. Neural networks in t27: from one neuron to a transformer block
+
  1. t27 basics: the language in 27 lessons
  2. From zero to a chip: FPGA programming with t27
  3. AI numbers with t27: from number formats to a ternary network
  4. GoldenFloat with t27: one rule, seventeen float widths
  5. Verifying hardware with t27: testbenches, waveforms, formal, mutation
  6. Clocking, resets and clock-domain crossings with t27
  7. Buses and peripherals with t27: UART, SPI, APB, AXI4, Ethernet
  8. The ternary machine: from a trit to a running program
  9. Arithmetic and DSP on the FPGA: from the carry chain to a filter
  10. Neural networks in t27: from one neuron to a transformer block
  11. The Queen and the hive: how the t27 project builds itself

All lessons

diff --git a/apps/website/public/learn/inherited-red/card.png b/apps/website/public/learn/inherited-red/card.png new file mode 100644 index 0000000000..c9f6bb125b Binary files /dev/null and b/apps/website/public/learn/inherited-red/card.png differ diff --git a/apps/website/public/learn/inherited-red/index.html b/apps/website/public/learn/inherited-red/index.html new file mode 100644 index 0000000000..cce913432e --- /dev/null +++ b/apps/website/public/learn/inherited-red/index.html @@ -0,0 +1,377 @@ + + + + + + +Inherited red: the Queen and the hive course, lesson 15 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Inherited red

+

You will learn

When a red check on a pull request is inherited from master, and why a cancelled run says nothing.

+

A bee may approve a PR with a red check when that check is red on master too. On 2026-10-06 a PR merged red because master's run of that check was cancelled and was read as red. Now the merger reads master itself, newest run first: run_says counts only a completed failure or success, look_back passes over everything else and over runs older than the merge base, and discount_holds needs the bee's word and MASTER RED.

+

Try it

Make every run cancelled; then move the failure before the merge base and look at the gate.

+

Open the interactive lesson →

+
Inherited red: what master's runs say about a red check
Inherited red: what master's runs say about a red check ↗
+

The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts.

+

specs/queen/merger_gate.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved
+; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq).
+; It does not import this spec, and t27c gen-js lowers no function bodies (#7300).
+; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario
+; asserts the verdict that scenario pins for the workflow.
+; Negative controls: discounted required check, unconcluded check, required check never posted,
+; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run
+; of the same check was CANCELLED (#7317)
+; ASCII only (L3)
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenMergerGate;
+
+/// With the ruleset unreadable the merger cannot tell which checks are required,
+/// so every check counts as required and no red one is discountable (#5776 item 2).
+pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool {
+    return !ruleset_readable || required;
+}
+
+/// A red check passes only when it is not required, has concluded, and is discounted
+pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool {
+    return !required && concluded && discounted;
+}
+
+/// A required check passes when it is posted and green
+pub fn required_check_passes(posted: bool, green: bool) -> bool {
+    return posted && green;
+}
+
+/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks;
+/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check
+/// that does not count as required.
+pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool {
+    if (!concluded) { return false; }
+    if (green) { return true; }
+    return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted);
+}
+
+// ---------------------------------------------------------------------------
+// What master says about a discounted red (#7317)
+//
+// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its
+// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled
+// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it.
+// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the
+// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or
+// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts.
+
+/// One master run of the red check, as the merger reads its job's status and conclusion.
+/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code.
+pub const RUN_MISSING : u8 = 0;
+pub const RUN_FAILURE : u8 = 1;
+pub const RUN_SUCCESS : u8 = 2;
+pub const RUN_CANCELLED : u8 = 3;
+pub const RUN_SKIPPED : u8 = 4;
+pub const RUN_TIMED_OUT : u8 = 5;
+/// queued or in progress
+pub const RUN_RUNNING : u8 = 6;
+/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail
+pub const RUN_OTHER : u8 = 7;
+
+/// What master says about the PR's red.
+pub const MASTER_UNKNOWN : u8 = 0;
+/// a completed failure: the red is inherited
+pub const MASTER_RED : u8 = 1;
+/// a completed success: master was green, the red is the PR's own
+pub const MASTER_GREEN : u8 = 2;
+
+/// One run on its own. Only a completed failure or a completed success decides anything.
+pub fn run_says(run: u8) -> u8 {
+    if (run == RUN_FAILURE) { return MASTER_RED; }
+    if (run == RUN_SUCCESS) { return MASTER_GREEN; }
+    return MASTER_UNKNOWN;
+}
+
+/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN.
+/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not.
+/// A run older than the PR's merge base is not evidence about the code the PR started from.
+pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 {
+    if (so_far != MASTER_UNKNOWN) { return so_far; }
+    if (!at_or_after_merge_base) { return MASTER_UNKNOWN; }
+    return run_says(run);
+}
+
+/// A red is discounted only when the bee's approval names it AND master says red.
+/// This is the `discounted` that check_passes takes.
+pub fn discount_holds(bee_named: bool, master: u8) -> bool {
+    return bee_named && master == MASTER_RED;
+}
+
+/// The gate opens when at least one check has posted, none is still running,
+/// and none blocks. Zero checks is not zero failures.
+pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool {
+    return posted > 0 && running == 0 && blocking == 0;
+}
+
+// Tests for counts_as_required
+
+test counts_as_required_follows_the_ruleset_when_readable {
+    assert counts_as_required(true, true) == true;
+    assert counts_as_required(true, false) == false;
+}
+
+test counts_as_required_is_every_check_when_unreadable {
+    assert counts_as_required(false, false) == true;
+    assert counts_as_required(false, true) == true;
+}
+
+// Tests for red_check_passes
+
+// Positive case: non-required check that concluded and is discounted
+test red_check_passes_valid_discounted_non_required {
+    assert red_check_passes(false, true, true) == true;
+}
+
+// Negative control: discounted required check (required=true, cannot be discounted)
+test red_check_passes_discounted_required_fails {
+    assert red_check_passes(true, true, true) == false;
+}
+
+// Negative control: unconcluded check (concluded=false, even if discounted)
+test red_check_passes_unconcluded_fails {
+    assert red_check_passes(false, false, true) == false;
+}
+
+// Negative control: non-required check that concluded but not discounted
+test red_check_passes_non_discounted_fails {
+    assert red_check_passes(false, true, false) == false;
+}
+
+test red_check_passes_all_false_fails {
+    assert red_check_passes(true, false, false) == false;
+}
+
+// Tests for required_check_passes
+
+test required_check_passes_posted_and_green_passes {
+    assert required_check_passes(true, true) == true;
+}
+
+// Self-test scenario "required check never posted"
+test required_check_passes_not_posted_fails {
+    assert required_check_passes(false, true) == false;
+}
+
+test required_check_passes_posted_but_not_green_fails {
+    assert required_check_passes(true, false) == false;
+}
+
+test required_check_passes_neither_posted_nor_green_fails {
+    assert required_check_passes(false, false) == false;
+}
+
+// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted)
+
+test check_passes_green_advisory_and_green_required_pass {
+    assert check_passes(true, false, true, true, false) == true;
+    assert check_passes(true, true, true, true, false) == true;
+}
+
+// Self-test scenario "red advisory check, discounted in the bee's approval"
+test check_passes_red_advisory_discounted_passes {
+    assert check_passes(true, false, true, false, true) == true;
+}
+
+// Self-test scenario "red advisory check, not discounted"
+test check_passes_red_advisory_not_discounted_blocks {
+    assert check_passes(true, false, true, false, false) == false;
+}
+
+// Self-test scenario "red REQUIRED check, discounted anyway"
+test check_passes_red_required_discounted_blocks {
+    assert check_passes(true, true, true, false, true) == false;
+}
+
+// Self-test scenarios "a check still running" and "pending commit status blocks":
+// not concluded blocks whatever else is true, a discount included
+test check_passes_running_check_blocks {
+    assert check_passes(true, false, false, true, true) == false;
+    assert check_passes(true, false, false, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable: a discounted red check still blocks"
+test check_passes_unreadable_ruleset_discounted_red_blocks {
+    assert check_passes(false, false, true, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable, every check green"
+test check_passes_unreadable_ruleset_green_passes {
+    assert check_passes(false, false, true, true, false) == true;
+}
+
+// Tests for gate_open: (posted, running, blocking)
+
+// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks
+test gate_open_posted_checks_all_pass_opens {
+    assert gate_open(2, 0, 0) == true;
+    assert gate_open(1, 0, 0) == true;
+}
+
+// Negative control: zero checks is not zero failures
+test gate_open_zero_checks_stays_closed {
+    assert gate_open(0, 0, 0) == false;
+}
+
+// Self-test scenario "a check still running"
+test gate_open_running_check_keeps_it_closed {
+    assert gate_open(3, 1, 0) == false;
+}
+
+// Self-test scenario "two red checks, only one discounted"
+test gate_open_one_blocking_check_keeps_it_closed {
+    assert gate_open(4, 0, 1) == false;
+}
+
+// Tests for run_says, look_back and discount_holds (#7317)
+
+test only_a_completed_failure_says_red {
+    assert run_says(RUN_FAILURE) == MASTER_RED;
+    assert run_says(RUN_SUCCESS) == MASTER_GREEN;
+}
+
+test every_other_run_says_unknown {
+    assert run_says(RUN_MISSING) == MASTER_UNKNOWN;
+    assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN;
+    assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN;
+    assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN;
+    assert run_says(RUN_RUNNING) == MASTER_UNKNOWN;
+    assert run_says(RUN_OTHER) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master newest cancelled, older failure at or after the merge base"
+test look_back_passes_a_cancelled_run_to_an_older_failure {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED;
+}
+
+// Self-test scenario "master failure only before the merge base"
+test look_back_stops_at_the_merge_base {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN;
+    assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN;
+}
+
+// The newest decisive run wins, in both directions
+test look_back_keeps_the_newest_decisive_run {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN;
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED;
+    assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED;
+}
+
+test look_back_over_only_unknown_runs_stays_unknown {
+    assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master failure": discounted
+test discount_holds_on_a_master_failure {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true;
+}
+
+// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked
+test pr_red_while_master_cancelled_is_blocked {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false;
+}
+
+// Self-test scenario "master success": the red is the PR's own -> blocked
+test pr_red_while_master_green_is_blocked {
+    assert discount_holds(true, MASTER_GREEN) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false;
+}
+
+// Master red, but the bee never named the check: blocked
+test master_red_without_the_bees_line_is_blocked {
+    assert discount_holds(false, MASTER_RED) == false;
+    assert discount_holds(false, MASTER_UNKNOWN) == false;
+}
+
+invariant the_run_and_master_codes_never_move {
+    assert RUN_MISSING == 0;
+    assert RUN_FAILURE == 1;
+    assert RUN_SUCCESS == 2;
+    assert RUN_CANCELLED == 3;
+    assert RUN_SKIPPED == 4;
+    assert RUN_TIMED_OUT == 5;
+    assert RUN_RUNNING == 6;
+    assert RUN_OTHER == 7;
+    assert MASTER_UNKNOWN == 0;
+    assert MASTER_RED == 1;
+    assert MASTER_GREEN == 2;
+}
+
+invariant unknown_never_discounts {
+    assert discount_holds(true, MASTER_UNKNOWN) == false;
+    assert discount_holds(true, run_says(RUN_MISSING)) == false;
+    assert discount_holds(true, run_says(RUN_CANCELLED)) == false;
+    assert discount_holds(true, run_says(RUN_SKIPPED)) == false;
+    assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false;
+    assert discount_holds(true, run_says(RUN_RUNNING)) == false;
+    assert discount_holds(true, run_says(RUN_OTHER)) == false;
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/neural-networks/index.html b/apps/website/public/learn/neural-networks/index.html index 8b35b51a94..b9108c39d1 100644 --- a/apps/website/public/learn/neural-networks/index.html +++ b/apps/website/public/learn/neural-networks/index.html @@ -63,7 +63,7 @@

Neural networks in t27: from one neuron to a transformer block

The agents of this project think with neural networks, and here their parts are written as t27 specs with tests, specs the compiler checks. Every widget runs the fn bodies of a specs/ml spec compiled to WebAssembly: move a slider and the spec answers.

Course 10, 27 lessons, a live widget and a t27 spec each: activations, softmax and embeddings, pooling and batch norm, losses, SGD, RMSProp and Adam, backpropagation and XOR, recurrent cells, attention, a transformer block and PPO. Every number is computed by a compiled spec.

Open the interactive course →

- +

All lessons

diff --git a/apps/website/public/learn/no-credit-for-keys/card.png b/apps/website/public/learn/no-credit-for-keys/card.png new file mode 100644 index 0000000000..c3592f2090 Binary files /dev/null and b/apps/website/public/learn/no-credit-for-keys/card.png differ diff --git a/apps/website/public/learn/no-credit-for-keys/index.html b/apps/website/public/learn/no-credit-for-keys/index.html new file mode 100644 index 0000000000..9ab62a8514 --- /dev/null +++ b/apps/website/public/learn/no-credit-for-keys/index.html @@ -0,0 +1,258 @@ + + + + + + +No credit for keys: the Queen and the hive course, lesson 19 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

No credit for keys

+

You will learn

What a contributor's lent keys earn, and which limits keep them safe.

+

Anyone can lend the hive model keys through queen-contributor-keys.t27. A key is probed before it is enabled: at most 4 probes at once, a new probe waits out 60 s after the last, and an owner holds at most 100 keys. Lending earns nothing by itself: CREDIT_WALLET is false, so wallet_credit answers 0 for any number of keys. XP follows the work the Queen records.

+

Try it

Drag the keys from 0 to 100 and watch the credit meter; then find the second a probe may start.

+

Open the interactive lesson →

+
Lending a key earns no credit: XP follows recorded work
Lending a key earns no credit: XP follows recorded work ↗
+

A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s.

+

specs/automation/queen-contributor-keys.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/queen-contributor-keys.t27 -- private contributor key lifecycle.
+// Host: BrowserOS trios/agent-server/apps/server/src/api/services/queen-contributor-keys.ts.
+// Trace: gHashTag/t27#5472; gHashTag/999-multibots-telegraf#3251.
+// Generated: t27c gen-ts supplies the host policy constants.
+// Claim status: executable contract; PostgreSQL and live provider evidence are separate.
+// XP follows work recorded by the Queen. Adding credentials never credits a wallet.
+// Negative managed indices and nonnegative environment indices share one history.
+// v1 (2026-10-01): trusted proxy, persistent ownership and bounded provider probes.
+// v2 (2026-10-02): one model for all of an owner's keys of a provider, accepted only
+// after a key of that provider called a tool with it. Probe timeout 15 s -> 90 s:
+// z-ai/glm-5.3 on NVIDIA answered in 20-46 s and glm-4.5-flash on Z.ai in up to 87 s.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::queen_contributor_keys {
+    pub const KIND : str = "automation";
+    pub const ID : str = "queen-contributor-keys";
+    pub const REPO : str = "BrowserOS";
+    pub const VERSION : u8 = 2;
+
+    pub const PROBE_TIMEOUT_MS : u32 = 90000;
+    pub const PROBE_MAX_TOKENS : u16 = 16;
+    pub const MAX_PROBES_IN_FLIGHT : u16 = 4;
+    pub const PROBE_COOLDOWN_SECONDS : u16 = 60;
+    pub const MAX_KEYS_PER_OWNER : u16 = 100;
+    pub const MAX_KEY_BYTES : u16 = 4096;
+    pub const MAX_BODY_BYTES : u16 = 8192;
+    pub const LABEL_LIMIT : u16 = 80;
+    pub const PROXY_TOKEN_MIN_BYTES : u16 = 32;
+    pub const OWNER_REASSIGNMENT : bool = false;
+    pub const CREDIT_WALLET : bool = false;
+    pub const ENVIRONMENT_INDICES_COMPACTED : bool = false;
+    pub const NVIDIA_URL : str = "https://integrate.api.nvidia.com/v1";
+    pub const NVIDIA_MODEL : str = "nvidia/nemotron-3-ultra-550b-a55b";
+    pub const ZAI_URL : str = "https://api.z.ai/api/paas/v4";
+    pub const ZAI_MODEL : str = "glm-4.5-flash";
+    pub const MODEL_LIMIT : u16 = 128;
+    pub const MODEL_PROBE_MAX_TOKENS : u16 = 512;
+    pub const MODEL_CHECK_ATTEMPTS : u16 = 2;
+    pub const MODEL_LIST_LIMIT : u16 = 500;
+    pub const MODEL_LIST_CACHE_SECONDS : u16 = 600;
+
+    fn trusted_proxy(configured_bytes: u32, token_matches: bool, subject_valid: bool) -> bool {
+        return configured_bytes >= PROXY_TOKEN_MIN_BYTES && token_matches && subject_valid;
+    }
+
+    fn source_matches_index(managed: bool, index: i32) -> bool {
+        return (managed && index < 0) || (!managed && index >= 0);
+    }
+
+    fn may_add_key(owned: u32) -> bool {
+        return owned < MAX_KEYS_PER_OWNER;
+    }
+
+    fn may_start_probe(active: u32) -> bool {
+        return active < MAX_PROBES_IN_FLIGHT;
+    }
+
+    fn cooldown_passed(has_probe: bool, elapsed_seconds: u32) -> bool {
+        return !has_probe || elapsed_seconds > PROBE_COOLDOWN_SECONDS;
+    }
+
+    fn credential_fits(bytes: u32, printable_without_space: bool) -> bool {
+        return bytes > 0 && bytes <= MAX_KEY_BYTES && printable_without_space;
+    }
+
+    fn same_binding(subject_matches: bool, fingerprint_matches: bool) -> bool {
+        return subject_matches && fingerprint_matches;
+    }
+
+    fn enabled_after_probe(was_enabled: bool, invalid: bool, success: bool, request_enable: bool, same_revision: bool) -> bool {
+        if invalid { return false; }
+        if success && request_enable && same_revision { return true; }
+        return was_enabled;
+    }
+
+    fn model_fits(chars: u32, allowed_chars: bool) -> bool {
+        return chars > 0 && chars <= MODEL_LIMIT && allowed_chars;
+    }
+
+    fn may_switch_model(owns_provider_key: bool, called_tool: bool) -> bool {
+        return owns_provider_key && called_tool;
+    }
+
+    fn may_try_model_check(attempt: u32) -> bool {
+        return attempt < MODEL_CHECK_ATTEMPTS;
+    }
+
+    fn wallet_credit(added: u32) -> u32 {
+        if CREDIT_WALLET { return added; }
+        return 0;
+    }
+
+    test "only a configured matching proxy and valid person subject are trusted" {
+        assert(trusted_proxy(32, true, true));
+        assert(!trusted_proxy(31, true, true));
+        assert(!trusted_proxy(32, false, true));
+        assert(!trusted_proxy(32, true, false));
+    }
+
+    test "managed indices are negative and environment history keeps its nonnegative indices" {
+        assert(source_matches_index(true, -1));
+        assert(!source_matches_index(true, 0));
+        assert(!source_matches_index(false, -1));
+        assert(source_matches_index(false, 0));
+        assert(source_matches_index(false, 10000));
+        assert(!ENVIRONMENT_INDICES_COMPACTED);
+    }
+
+    test "the owner key limit is checked before another key is stored" {
+        assert(may_add_key(0));
+        assert(may_add_key(99));
+        assert(!may_add_key(100));
+    }
+
+    test "probe fanout and per-key cooldown are bounded" {
+        assert(may_start_probe(3));
+        assert(!may_start_probe(4));
+        assert(cooldown_passed(false, 0));
+        assert(!cooldown_passed(true, 59));
+        assert(!cooldown_passed(true, 60));
+        assert(cooldown_passed(true, 61));
+    }
+
+    test "credentials are bounded nonempty printable values" {
+        assert(credential_fits(1, true));
+        assert(credential_fits(4096, true));
+        assert(!credential_fits(0, true));
+        assert(!credential_fits(4097, true));
+        assert(!credential_fits(32, false));
+    }
+
+    test "an existing binding cannot change its owner or credential" {
+        assert(same_binding(true, true));
+        assert(!same_binding(false, true));
+        assert(!same_binding(true, false));
+        assert(!OWNER_REASSIGNMENT);
+    }
+
+    test "a failed probe disables an invalid key and a concurrent disable wins" {
+        assert(!enabled_after_probe(true, true, false, false, true));
+        assert(enabled_after_probe(false, false, true, true, true));
+        assert(!enabled_after_probe(false, false, true, true, false));
+        assert(!enabled_after_probe(false, false, true, false, true));
+        assert(enabled_after_probe(true, false, false, false, true));
+    }
+
+    test "adding credentials never credits a wallet" {
+        assert(wallet_credit(1) == 0);
+        assert(wallet_credit(100) == 0);
+    }
+
+    test "a model name is a bounded nonempty identifier" {
+        assert(model_fits(1, true));
+        assert(model_fits(128, true));
+        assert(!model_fits(0, true));
+        assert(!model_fits(129, true));
+        assert(!model_fits(12, false));
+    }
+
+    test "a model switch needs an owned key of the provider and a tool call" {
+        assert(may_switch_model(true, true));
+        assert(!may_switch_model(false, true));
+        assert(!may_switch_model(true, false));
+    }
+
+    test "a busy or refused key passes the model check to the next one, a bounded number of times" {
+        assert(may_try_model_check(0));
+        assert(may_try_model_check(1));
+        assert(!may_try_model_check(2));
+    }
+
+    test "a probe waits long enough for a slow reasoning model" {
+        assert(PROBE_TIMEOUT_MS >= 60000);
+        assert(MODEL_PROBE_MAX_TOKENS > PROBE_MAX_TOKENS);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 2);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/one-task-three-kinds/card.png b/apps/website/public/learn/one-task-three-kinds/card.png new file mode 100644 index 0000000000..b53dcb47a4 Binary files /dev/null and b/apps/website/public/learn/one-task-three-kinds/card.png differ diff --git a/apps/website/public/learn/one-task-three-kinds/index.html b/apps/website/public/learn/one-task-three-kinds/index.html new file mode 100644 index 0000000000..1397a36013 --- /dev/null +++ b/apps/website/public/learn/one-task-three-kinds/index.html @@ -0,0 +1,224 @@ + + + + + + +One task, three kinds: the Queen and the hive course, lesson 3 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

One task, three kinds

+

You will learn

How an issue, a review and a job become one kind of task with seven states, and how one filter reads them.

+

The Queen runs three kinds of work: issues for bees, reviews of finished work, and jobs. Each kept its own codes, so no board could show them together. tasks.t27 gives them 7 shared states, numbered as the board's columns plus failed. A cancelled job shows as dropped, and so does a review row that was forgotten. task_matches is the filter the board, the game and the route all apply: a bit per kind, a bit per state, and 0 means any.

+

Try it

Show only failed tasks of every kind; then find which job state shows as dropped.

+

Open the interactive lesson →

+
One task, three kinds: the filter the board and the game share
One task, three kinds: the filter the board and the game share ↗
+

An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through.

+

specs/queen/tasks.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/tasks.t27 -- one task, many kinds, and the bees working on them, as everyone reads them
+; Owner, 2026-10-08 (translated): lower every task of the project to the scheduler's native level; a
+; dashboard shows all of it with any filter (https://app.t27.ai/game/kanban); the bees fly over the
+; GitHub issue each is working on in the game (https://app.t27.ai/game). Epic #7718, slice 1.
+; Measured that morning, four surveys:
+; - there is no single task. An issue dispatch (dispatch.t27 COL_*), a job (jobs.t27 J_*) and an app
+;   review (app.t27, per head) each kept their own codes.
+; - no public field said which bee is on which issue. The game drew a public snapshot with no bees, and
+;   its older comb paired the k-th busy slot with the k-th running card, which is a rank, not a link.
+; This card is the view everyone reads: the supervisor builds GET /queen/public-tasks with it, and the
+; board and the game filter and draw with it. The kinds keep their own machines; this only says how
+; each one shows.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenTasks;
+
+/// Kinds of task. Index into KIND_NAMES.
+pub const TK_ISSUE : u8 = 0;
+pub const TK_REVIEW : u8 = 1;
+pub const TK_JOB : u8 = 2;
+pub const TASK_KINDS : u8 = 3;
+pub const KIND_NAMES : [3]str = ["issue", "review", "job"];
+
+/// One state for every kind, numbered as dispatch.t27's board columns (COL_BACKLOG .. COL_DROPPED) so
+/// an issue's column IS its state, plus FAILED, which a column never was. Index into STATE_NAMES.
+pub const TS_BACKLOG : u8 = 0;
+pub const TS_BLOCKED : u8 = 1;
+pub const TS_RUNNING : u8 = 2;
+pub const TS_REVIEW : u8 = 3;
+pub const TS_DONE : u8 = 4;
+pub const TS_DROPPED : u8 = 5;
+pub const TS_FAILED : u8 = 6;
+pub const TASK_STATES : u8 = 7;
+pub const STATE_NAMES : [7]str = ["backlog", "blocked", "running", "review", "done", "dropped", "failed"];
+
+/// A job's state (jobs.t27: 0 running, 1 done, 2 failed, 3 cancelled) as a task state.
+pub fn job_state(j: u8) -> u8 {
+    if (j == 0) { return TS_RUNNING; }
+    if (j == 1) { return TS_DONE; }
+    if (j == 2) { return TS_FAILED; }
+    return TS_DROPPED;
+}
+
+/// An app review's row state (queued 0, posted 1, forgotten 2, failed 3) as a task state.
+pub fn review_state(r: u8) -> u8 {
+    if (r == 0) { return TS_BACKLOG; }
+    if (r == 1) { return TS_DONE; }
+    if (r == 2) { return TS_DROPPED; }
+    return TS_FAILED;
+}
+
+/// The A2A v1.0 task state each state is, for the day the Queen answers A2A at her edge (epic #7718
+/// slice 7). A task waiting for a judge or a person is input-required; dropped is canceled.
+pub const A2A_SUBMITTED : u8 = 1;
+pub const A2A_WORKING : u8 = 2;
+pub const A2A_INPUT_REQUIRED : u8 = 3;
+pub const A2A_COMPLETED : u8 = 4;
+pub const A2A_CANCELED : u8 = 5;
+pub const A2A_FAILED : u8 = 6;
+
+pub fn a2a_state(s: u8) -> u8 {
+    if (s == TS_BACKLOG) { return A2A_SUBMITTED; }
+    if (s == TS_RUNNING) { return A2A_WORKING; }
+    if (s == TS_BLOCKED or s == TS_REVIEW) { return A2A_INPUT_REQUIRED; }
+    if (s == TS_DONE) { return A2A_COMPLETED; }
+    if (s == TS_DROPPED) { return A2A_CANCELED; }
+    return A2A_FAILED;
+}
+
+/// A bee is one dispatch still in flight: offered and not claimed yet (QUEUED), heard from within
+/// BEE_QUIET_SECONDS (WORKING), or silent longer (QUIET). The game draws a quiet bee hovering, not
+/// working: a bee that has not spoken for minutes is not shown as busy.
+pub const BS_QUEUED : u8 = 0;
+pub const BS_WORKING : u8 = 1;
+pub const BS_QUIET : u8 = 2;
+pub const BEE_QUIET_SECONDS : u32 = 180;
+pub const BEE_STATE_NAMES : [3]str = ["queued", "working", "quiet"];
+
+/// `claimed`: a runner took the order (or the container started it); `heard`: an event was recorded
+/// for its conversation; `seconds_since`: since that event, or since it started when none was heard.
+pub fn bee_state(claimed: bool, seconds_since: u32) -> u8 {
+    if (claimed == false) { return BS_QUEUED; }
+    if (seconds_since > BEE_QUIET_SECONDS) { return BS_QUIET; }
+    return BS_WORKING;
+}
+
+/// The filter the board, the game and the route all apply: a bit per kind and a bit per state, and 0
+/// meaning "any". Repository and text are matched beside it; they are strings, and equality on
+/// strings is the caller's.
+pub fn task_matches(kind_mask: u32, state_mask: u32, kind: u8, state: u8) -> bool {
+    if (kind >= TASK_KINDS or state >= TASK_STATES) { return false; }
+    if (kind_mask != 0 and (kind_mask & (1 << kind)) == 0) { return false; }
+    if (state_mask != 0 and (state_mask & (1 << state)) == 0) { return false; }
+    return true;
+}
+
+/// Tasks one answer carries at most; the route pages past it.
+pub const TASK_PAGE_MAX : u32 = 2000;
+pub const TASK_PAGE_DEFAULT : u32 = 500;
+
+test a_job_and_a_review_show_as_one_lifecycle {
+    assert job_state(0) == TS_RUNNING;
+    assert job_state(1) == TS_DONE;
+    assert job_state(2) == TS_FAILED;
+    assert job_state(3) == TS_DROPPED;
+    assert review_state(0) == TS_BACKLOG;
+    assert review_state(1) == TS_DONE;
+    assert review_state(2) == TS_DROPPED;
+    assert review_state(3) == TS_FAILED;
+}
+
+test every_state_has_an_a2a_state {
+    assert a2a_state(TS_BACKLOG) == A2A_SUBMITTED;
+    assert a2a_state(TS_RUNNING) == A2A_WORKING;
+    assert a2a_state(TS_BLOCKED) == A2A_INPUT_REQUIRED;
+    assert a2a_state(TS_REVIEW) == A2A_INPUT_REQUIRED;
+    assert a2a_state(TS_DONE) == A2A_COMPLETED;
+    assert a2a_state(TS_DROPPED) == A2A_CANCELED;
+    assert a2a_state(TS_FAILED) == A2A_FAILED;
+}
+
+test a_bee_is_queued_working_or_quiet {
+    assert bee_state(false, 0) == BS_QUEUED;
+    assert bee_state(false, 9999) == BS_QUEUED;
+    assert bee_state(true, 0) == BS_WORKING;
+    assert bee_state(true, BEE_QUIET_SECONDS) == BS_WORKING;
+    assert bee_state(true, BEE_QUIET_SECONDS + 1) == BS_QUIET;
+}
+
+test a_filter_narrows_and_zero_means_any {
+    assert task_matches(0, 0, TK_ISSUE, TS_RUNNING) == true;
+    assert task_matches(1 << TK_ISSUE, 0, TK_ISSUE, TS_DONE) == true;
+    assert task_matches(1 << TK_JOB, 0, TK_ISSUE, TS_DONE) == false;
+    assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_RUNNING) == true;
+    assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_BACKLOG) == false;
+    assert task_matches((1 << TK_ISSUE) | (1 << TK_JOB), (1 << TS_RUNNING) | (1 << TS_REVIEW), TK_JOB, TS_REVIEW) == true;
+    // an unknown kind or state matches nothing, even with no filter
+    assert task_matches(0, 0, TASK_KINDS, TS_DONE) == false;
+    assert task_matches(0, 0, TK_ISSUE, TASK_STATES) == false;
+}
+
+invariant the_states_are_the_board_columns {
+    assert TS_BACKLOG == 0 and TS_BLOCKED == 1 and TS_RUNNING == 2;
+    assert TS_REVIEW == 3 and TS_DONE == 4 and TS_DROPPED == 5 and TS_FAILED == 6;
+    assert TK_ISSUE == 0 and TK_REVIEW == 1 and TK_JOB == 2;
+    assert TASK_PAGE_DEFAULT <= TASK_PAGE_MAX;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/ppo-and-the-reward/index.html b/apps/website/public/learn/ppo-and-the-reward/index.html index 636b8e51fb..d092d2e233 100644 --- a/apps/website/public/learn/ppo-and-the-reward/index.html +++ b/apps/website/public/learn/ppo-and-the-reward/index.html @@ -263,7 +263,7 @@

PPO and the reward

invariant ppo_clip_loss_constraint_4 assert clipped_surrogate(0.1, -1.0, 0.2) == -0.8

Open the lesson's spec in the player ↗

- +

All lessons

diff --git a/apps/website/public/learn/queen-and-hive.t27 b/apps/website/public/learn/queen-and-hive.t27 new file mode 100644 index 0000000000..5d7850736c --- /dev/null +++ b/apps/website/public/learn/queen-and-hive.t27 @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/course/queen-and-hive.t27 -- the t27 course on the Queen and the hive, how the project builds itself +; Source of truth for apps/website/scripts/course-from-spec.mjs (gHashTag/trinity), which reads +; it the way it reads specs/course/course.t27: the constant schema, every test block below, every +; widget in specs/widgets/gallery.t27, every lesson spec compiled clean, the Russian bundle named +; by specs/course/queen-and-hive-ru.t27. It writes this course into src/lib/course.generated.ts +; (the page at #/queen-and-hive) and a byte-identical copy of this file at +; public/learn/queen-and-hive.t27. ASCII only (L3), English only (LANG-EN). + +; WHAT THE COURSE IS: course 11 of specs/course/courses.t27, 27 lessons in 9 modules of 3: the +; task, who goes next, a bee's life, the reviewer bee, the review jam, keys, credit and money, +; agents among themselves, and night and the board. The project builds itself: an issue becomes +; a task for a bee, the bee writes a .t27 spec, a reviewer bee checks it, it merges and the site +; regenerates. The Queen that runs this loop is written in gHashTag/t27 specs/queen and +; specs/automation, each spec with numeric tests. Every widget runs the fn bodies of one of 15 of +; those specs compiled to WebAssembly (public/widgets//logic.js, held to its spec by +; scripts/widget-logic.mjs), vendored byte for byte from t27 master c42f8fb40. The door code +; around review-jam's fns comes from gHashTag/999-multibots-telegraf#4020. + +; WHAT THE COURSE DOES NOT DO: it reads no live Queen. Every number in a lesson is a constant or +; a recorded measurement of the lesson spec, or an answer of its compiled fns to inputs the +; reader sets. Lesson 26 shows a finding of dispatch.t27 as the board draws it today: a closed +; issue's finished claims all land in done. + +; WHY 27: 3 cubed lessons, the size of a TRI-27 word. Changing the shape changes the tests. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module queen_and_hive; + +pub const KIND : str = "course"; +pub const ID : str = "queen-and-hive"; +pub const SCHEMA_VERSION : u8 = 1; +; The derived files the generator rewrites (relative to apps/website). +pub const GENERATED : [2]str = ["src/lib/course.generated.ts", "public/learn/queen-and-hive.t27"]; +pub const ROUTE : str = "queen-and-hive"; +; Share pages: learn/ for course 1, learn// for the next (specs/course/courses.t27). +pub const SHARE_PATH : str = "learn/queen-and-hive/"; +; The paid training every course page links to (specs/course/arithmetic-and-dsp.t27). +pub const COHORT_ROUTE : str = "fpga-training"; +pub const GALLERY : str = "specs/widgets/gallery.t27"; +pub const LOCALES : [2]str = ["en", "ru"]; +pub const RU_CONTRACT : str = "specs/course/queen-and-hive-ru.t27"; +; Lesson marks the reader sets are kept in this browser only, under this key, one key for +; every course. +pub const PROGRESS_KEY : str = "t27-course-done"; +pub const SENDS_NOTHING : bool = true; +; A lesson's widget is the page's main stage, so it is drawn taller than a +; gallery card (specs/widgets/gallery.t27 EMBED_* heights are for embeds in posts). +pub const TOOL_FRAME_HEIGHT : u16 = 760; +pub const PLAYER_FRAME_HEIGHT : u16 = 480; + +; --- Words on the page --------------------------------------------------------------------- +pub const TITLE : str = "The Queen and the hive: how the t27 project builds itself"; +pub const DESCRIPTION : str = "Course 11, 27 lessons, a live widget and a t27 spec each: shaping an issue, priority and review first, a task's life, the reviewer and the merge gate, the review jam, keys, credit and tokens, agents in one group, the night watch and the board. Every answer comes from a compiled spec."; +pub const SAY_KICKER : str = "Course"; +pub const SAY_LEAD : str = "This project builds itself: an issue becomes a task for a bee, the bee writes a t27 spec, a reviewer bee checks it, it merges, and the site regenerates. The Queen who runs that loop is written as t27 specs with tests, and every widget here runs the fn bodies of one of them compiled to WebAssembly."; +pub const SAY_SHAPE : str = "9 modules of 3 lessons, 27 cells. A filled cell is a lesson you marked done."; +pub const SAY_START : str = "Start lesson 1"; +pub const SAY_CONTINUE : str = "Continue"; +pub const SAY_MODULE : str = "Module {0}"; +pub const SAY_LESSON : str = "Lesson {0} of {1}"; +pub const SAY_GOAL : str = "You will learn"; +pub const SAY_TRY : str = "Try it"; +pub const SAY_ALSO : str = "Also try"; +pub const SAY_ALSO_HINT : str = "Each one swaps the widget above."; +pub const SAY_BACK_TO_MAIN : str = "Back to this lesson's widget"; +pub const SAY_SPEC : str = "Open the lesson's spec in the player"; +pub const SAY_OPEN_PAGE : str = "Open the widget on its own page"; +pub const SAY_WIDGET_LANG : str = "Widgets keep their own English words: each one lives in its own t27 spec."; +pub const SAY_PREV : str = "Previous"; +pub const SAY_NEXT : str = "Next"; +pub const SAY_ALL : str = "All lessons"; +pub const SAY_MARK : str = "Mark as done"; +pub const SAY_MARKED : str = "Done"; +pub const SAY_PROGRESS : str = "{0} of {1} done"; +pub const SAY_PRIVATE : str = "Progress stays in this browser; nothing is sent."; +pub const SAY_SOURCE : str = "This course is itself a t27 spec: read it"; +pub const SAY_COHORT : str = "Looking for the paid FPGA training? It moved to its own page."; +pub const SAY_NOT_FOUND : str = "No lesson has this address."; +pub const SAY_OPEN_LESSON : str = "Open the interactive lesson"; +pub const SAY_OPEN_COURSE : str = "Open the interactive course"; +pub const SAY_SEO_TITLE : str = "{0}: the Queen and the hive course, lesson {1} of {2}"; +pub const SAY_SHARE : str = "Link to share, with a preview card"; +pub const SAY_NEXT_COURSE : str = "Next course"; +pub const SAY_PREV_COURSE : str = "Previous course"; + +; --- Modules ------------------------------------------------------------------------------- +pub const LESSONS_PER_MODULE : u8 = 3; +pub const MODULE_COUNT : u8 = 9; +pub const MODULE_IDS : [9]str = ["the-task", "who-goes-next", "a-bees-life", "the-reviewer", "the-jam", "keys", "credit-and-money", "agents-together", "night-and-the-board"]; +pub const MODULE_TITLES : [9]str = [ + "The task", + "Who goes next", + "A bee's life", + "The reviewer bee", + "The review jam", + "Keys", + "Credit and money", + "Agents among themselves", + "Night and the board" +]; +pub const MODULE_LINES : [9]str = [ + "An issue becomes work only once it is shaped: the sections it lacks are written or asked for, its boundary names real files, and one filter reads every kind of task.", + "The order the Queen takes issues in: a label ages one step, at most 3 criticals run as critical, and nothing new starts while finished work waits for review.", + "From candidate to done: twelve states and sixteen events, a ship in the game that flies home, and a deploy that does not cut a bee short.", + "Finished work is judged on its own clock: enough reviews at once to keep up, a pause that doubles for a row that waits, and a merge gate that zero checks cannot open.", + "A pull request that will not move is in exactly one of ten cases, each with the Queen's move, and a red check is inherited only when master says so.", + "A bee's model key can be refused: the code in the body sorts the refusal, a transient one rests the key for seconds, and the bee moves on to the next free key.", + "Lending a key earns no credit, an operation is sold at twice its cost in half-cent tokens, and tokens burn 30 days after they land.", + "One bot per agent in one group: who is called, a chain three bot messages deep, and the clocks that keep agents from flooding a chat.", + "What the hive shows: a night watch that opens, comments on and closes issues, the columns of the public board, and the comb that flashes for each event." +]; + +; --- Lessons ------------------------------------------------------------------------------- +; LESSON_WIDGETS names the widget the lesson opens; every one is a gallery id, and no two +; lessons of any course open the same one. LESSON_ALSO offers module siblings and related hive +; widgets. LESSON_SPECS is the spec under public/t27/files/ the lesson opens in the player, and +; the spec whose compiled fn bodies the widget runs. +pub const LESSON_COUNT : u8 = 27; +pub const LESSON_IDS : [27]str = [ + "shaping-an-issue", + "a-boundary-of-real-files", + "one-task-three-kinds", + "aging-a-label", + "the-queue", + "review-first", + "a-tasks-life", + "the-ship-flies-home", + "a-deploy-mid-flight", + "enough-reviewers", + "back-off", + "the-merge-gate", + "the-review-jam", + "the-queens-move", + "inherited-red", + "a-refused-key", + "a-key-rests", + "the-next-key", + "no-credit-for-keys", + "twice-the-cost", + "tokens-that-burn", + "who-is-called", + "three-bots-deep", + "stop-and-mute", + "the-night-watch", + "the-public-board", + "the-comb-answers" +]; +pub const LESSON_MODULES : [27]str = [ + "the-task", + "the-task", + "the-task", + "who-goes-next", + "who-goes-next", + "who-goes-next", + "a-bees-life", + "a-bees-life", + "a-bees-life", + "the-reviewer", + "the-reviewer", + "the-reviewer", + "the-jam", + "the-jam", + "the-jam", + "keys", + "keys", + "keys", + "credit-and-money", + "credit-and-money", + "credit-and-money", + "agents-together", + "agents-together", + "agents-together", + "night-and-the-board", + "night-and-the-board", + "night-and-the-board" +]; +pub const LESSON_WIDGETS : [27]str = [ + "hive-shape", + "hive-boundary", + "hive-task-filter", + "hive-aging", + "hive-rank", + "hive-review-first", + "hive-lifecycle", + "hive-ship", + "hive-drain", + "hive-reviewers", + "hive-revisit", + "hive-gate", + "hive-jam", + "hive-jam-action", + "hive-master-red", + "hive-key-error", + "hive-key-rest", + "hive-next-key", + "hive-credit", + "hive-price", + "hive-burn", + "hive-called", + "hive-chain", + "hive-cooldown", + "hive-night", + "hive-column", + "hive-cell" +]; +pub const LESSON_ALSO : [27]str = [ + "hive-boundary,hive-task-filter", + "hive-shape,hive-task-filter", + "hive-shape,hive-column,hive-lifecycle", + "hive-rank,hive-review-first", + "hive-aging,hive-review-first", + "hive-rank,hive-reviewers", + "hive-ship,hive-drain,hive-column", + "hive-lifecycle,hive-cell", + "hive-lifecycle,hive-ship", + "hive-revisit,hive-gate,hive-review-first", + "hive-reviewers,hive-key-rest", + "hive-master-red,hive-reviewers", + "hive-jam-action,hive-master-red", + "hive-jam,hive-gate", + "hive-gate,hive-jam", + "hive-key-rest,hive-next-key", + "hive-key-error,hive-next-key,hive-revisit", + "hive-key-error,hive-key-rest,hive-credit", + "hive-price,hive-next-key", + "hive-burn,hive-credit", + "hive-price,hive-night", + "hive-chain,hive-cooldown", + "hive-called,hive-cooldown", + "hive-chain,hive-called", + "hive-column,hive-cell", + "hive-task-filter,hive-lifecycle,hive-night", + "hive-ship,hive-night" +]; +pub const LESSON_SPECS : [27]str = [ + "specs/queen/shaping.t27", + "specs/queen/shaping.t27", + "specs/queen/tasks.t27", + "specs/queen/priority.t27", + "specs/queen/priority.t27", + "specs/queen/review_first.t27", + "specs/queen/dispatch.t27", + "specs/queen/hive.t27", + "specs/queen/drain.t27", + "specs/queen/reviewer.t27", + "specs/queen/reviewer.t27", + "specs/queen/merger_gate.t27", + "specs/automation/review-jam.t27", + "specs/automation/review-jam.t27", + "specs/queen/merger_gate.t27", + "specs/queen/key_failover.t27", + "specs/queen/key_failover.t27", + "specs/queen/key_failover.t27", + "specs/automation/queen-contributor-keys.t27", + "specs/automation/token-wallet.t27", + "specs/automation/token-wallet.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/card-chat-night-watch.t27", + "specs/queen/dispatch.t27", + "specs/queen/hive.t27" +]; +pub const LESSON_TITLES : [27]str = [ + "Shaping an issue", + "A boundary of real files", + "One task, three kinds", + "Aging a label", + "The queue", + "Review first", + "A task's life", + "The ship flies home", + "A deploy mid-flight", + "Enough reviewers", + "Back off", + "The merge gate", + "The review jam", + "The Queen's move", + "Inherited red", + "A refused key", + "A key rests", + "The next key", + "No credit for keys", + "Twice the cost", + "Tokens that burn", + "Who is called", + "Three bots deep", + "Stop and mute", + "The night watch", + "The public board", + "The comb answers" +]; +pub const LESSON_GOALS : [27]str = [ + "What the Queen does with an issue that lacks sections, and why she stops after two tries.", + "Which proposed paths a task may reserve, and why one invented path stops the whole shape.", + "How an issue, a review and a job become one kind of task with seven states, and how one filter reads them.", + "How a labelled issue moves up with age, and why only by one level.", + "How the cap, aging and blockers order a queue of open issues.", + "Why the Queen closes the backlog while finished work waits for a review, and what still goes.", + "Which states a dispatched task passes through, and which events are legal in each.", + "Where the game draws a bee for each thing it is doing, and how a finished task ends on screen.", + "What a redeploy does to a bee, a review, a round, a job and a wait.", + "How many reviews must run at once for the reviewer to keep up with the bees.", + "Why a row that answers wait is visited less and less often, and how often at least.", + "When the merger may merge a pull request a reviewer bee approved.", + "Which of ten cases a stuck pull request is in, and why the earliest matching rule wins.", + "What the Queen does for each jam, and which jams she waits on.", + "When a red check on a pull request is inherited from master, and why a cancelled run says nothing.", + "How the Queen sorts a provider's refusal into transient, quota, auth or a bad request.", + "How long a refused key rests before a bee may use it again.", + "Which key a bee tries next, and when it moves to another pool or gives up.", + "What a contributor's lent keys earn, and which limits keep them safe.", + "How the project prices an operation and what a month of tokens buys.", + "When a batch of tokens burns, and which batch is spent first.", + "Which messages in the hive group wake an agent, from a person and from another agent's bot.", + "How the hive stops agents from answering each other forever.", + "What /stop and /mute do, and why neither can lose a person or a direct call.", + "What the night watch does in the morning with four card checks.", + "Which column the public board draws each claim in, and the case it gets wrong.", + "How the game shows that something happened to a task, and how often the map grows." +]; +pub const LESSON_TEXTS : [27]str = [ + "A bee can start only on an issue that says what to do and which files it may touch. The Queen holds every open issue to judge() of specs/policy/issue_shape.t27. One that is not ready she shapes: a model writes the missing sections, and the body goes back only if judge() then finds nothing missing. After SHAPE_ATTEMPT_LIMIT, 2 failed shapes, she stops and comments once with what is missing, and an epic is never shaped. shape_action in shaping.t27 decides, and a new try waits 360 minutes.", + "A task's boundary is a reservation: while a bee works, no other bee takes those files. So a wrong boundary is worse than a missing one, because it holds files the work does not own. path_accepted takes any file that exists, and a new file only when its folder exists, the issue cites it, and its language is one the only-t27 rule allows. shape_lands writes the shape back only when no section is missing, it names 1 to 12 paths, and none of them was refused.", + "The Queen runs three kinds of work: issues for bees, reviews of finished work, and jobs. Each kept its own codes, so no board could show them together. tasks.t27 gives them 7 shared states, numbered as the board's columns plus failed. A cancelled job shows as dropped, and so does a review row that was forgotten. task_matches is the filter the board, the game and the route all apply: a bit per kind, a bit per state, and 0 means any.", + "Without labels the Queen takes issues in GitHub's listing order. priority.t27 adds four levels, critical, high, normal and low; lower runs first, and an unlabelled issue counts as normal. A labelled issue gains one level after AGING_DAYS, 14 days, and at most MAX_AGING_STEPS, 1 level, so a low issue reaches normal on day 14 and stays there. High and critical do not age. effective_level computes the line the widget draws.", + "Labels inflate: if every issue is critical, none is. capped_level lets at most CRITICAL_CAP, 3, issues of a listing run as critical, taken in listing order, and the 4th runs as high. An issue with an open blocker is skipped, not waited on, so it cannot hold up the queue. outranks compares the effective level, then the real level, then the place in the listing, so an aged issue never beats a real one of its level. why names the reason for each place.", + "On 2026-10-18 the status page read 69 of 70 workers busy, and in one hour 102 dispatches, 56 finished and 11 reviewed. Bees started about nine for every verdict, and each new bee held files that the bees in review needed when they were sent back. review_first.t27 closes the backlog while the review queue is longer than REVIEW_QUEUE_MAX, 0. Work the review sent back and work the owner assigned still go: admission admits them at any queue length.", + "dispatch.t27 is the Queen's task lifecycle, the cycle that chooses, starts, ends, reviews and releases work. A task has 12 states and 16 events. next_state answers every pair, and 99 means the event is illegal in that state: a verdict cannot reach a running task, and a reap cannot reach an ended one. After a verdict a send-back waits for its floor and is released; even an accept is sent back if CI turns red under the ceiling.", + "In the game at app.t27.ai/game each bee is a ship over the issue it works on. ship_mode in hive.t27 picks the place: parked at the entrance before a runner claims it, orbiting its cell while it is heard from, hovering when it has gone quiet, and waiting at its repository's portal until its cell is placed. When the bee leaves the feed it flies home, and ship_alpha fades it over DEPART_MS, 2400 ms: at 1200 ms it is at 500 of 1000.", + "Every merge redeploys the Queen, and work is running when it does. drain.t27 sorts that work. A round, a job step and a wait row are rows the store keeps: the process leaves them and the next process carries on. A review is stopped, because a stop loses at most one review, which is redone. A bee is held, because what it has done lives in its conversation; at the cap, drain_cap_seconds, 1800 s today, it is handed back and its work is salvaged into its branch.", + "A review may take REVIEW_ROW_SECONDS, 300 s, so one slot clears at most 12 rows an hour. On 2026-10-09 bees finished about 58 an hour while 111 rows waited, and the fixed REVIEWER_CONCURRENCY of 4 tops out at 48. slots_needed(58) is 5. reviewer_concurrency takes the keys, their lanes and the free memory, never goes above 16 and never below 1 while anything waits, and keeps_up compares it with the finish rate.", + "Over 44 minutes on 2026-10-09 the reviewer made 941 row visits, 888 of them wait, from 22 rows whose branch it could not read; one row was visited 54 times. revisit_after_seconds now waits 30 s after one wait and doubles per wait, never past 900 s: 30, 60, 120, 240, 480, 900. due_again lets the row be taken only once that time has passed, and any other answer resets the count.", + "The reviewer bee approves, and the merger merges; merger_gate.t27 is the rule between them. check_passes lets a green check through and blocks any check still running. A red one passes only when it is not required, has concluded, and the bee named it as discounted, and with the ruleset unreadable every check counts as required. gate_open needs at least 1 posted check, 0 running and 0 blocking: zero checks is not zero failures.", + "A pull request can stall for many reasons at once: refused, pushed again, and red on a noise check. review-jam.t27 gives the Queen one answer. jam_case checks 9 facts in a fixed order and the earliest rule that matches wins, so a PR the Queen could not measure is never reported as free, and one that was refused and then pushed is not taken for noise. There are 10 cases, from none to unmeasured.", + "Each case has one action at the same position: no reviewer dispatches a reviewer, refused not fixed dispatches the coder, refused then pushed dispatches a re-review, and labeled not merged finishes the merge. jam_action maps any value above 9 to measure again, so an unknown case is never nothing to do. jam_is_queens_turn is false for 3 cases, none, draft or fresh, and stacked on an open PR: she waits on those.", + "A bee may approve a PR with a red check when that check is red on master too. On 2026-10-06 a PR merged red because master's run of that check was cancelled and was read as red. Now the merger reads master itself, newest run first: run_says counts only a completed failure or success, look_back passes over everything else and over runs older than the merge base, and discount_holds needs the bee's word and MASTER RED.", + "Bees call models with keys, and keys get refused. key_failover.t27 sorts each refusal with error_kind. The z.ai code in the body wins over the HTTP status, because z.ai sends a spent quota and a passing overload with the same 429: code 1305 is transient, 1113 is quota. No answer at all is transient. A quota or auth refusal closes the key, and only a bad request is charged to the issue.", + "A transient refusal rests the key COOL_BASE_SECONDS, 15 s, doubled per strike in a row and never more than COOL_CAP_SECONDS, 240 s: 15, 30, 60, 120, 240. A spent quota or a refused key closes it for KEY_BREAK_SECONDS, 1800 s. cooldown_seconds gives all of it. The rest is short so that 34 keys are never all resting through a one-minute burst like the one of 2026-10-17.", + "On 2026-10-17 a bee started on key 9 of 10 and died 7 s later while 33 other keys stood by: it got one key at dispatch and kept it. next_free now picks the next key after the last one that is neither tried nor resting. 1305 is an overload of the pool's model, so once POOL_OVERLOAD_KEYS, 3, keys said it within a minute, step moves the bee to the next pool. After 8 attempts the turn ends.", + "Anyone can lend the hive model keys through queen-contributor-keys.t27. A key is probed before it is enabled: at most 4 probes at once, a new probe waits out 60 s after the last, and an owner holds at most 100 keys. Lending earns nothing by itself: CREDIT_WALLET is false, so wallet_credit answers 0 for any number of keys. XP follows the work the Queen records.", + "Every dollar becomes tokens at one rate, TOKEN_MICRO_USD 5000: a token is half a cent. price_tokens sells an operation at twice what it costs, MARKUP_PCT 200, and rounds up so nothing sells below that: a cost of 12300 micro-dollars is 5 tokens. month_tokens turns $20 into 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12.", + "The owner's rule of 2026-10-01: all money goes into tokens, and tokens not spent within the month burn, so a person who paid makes content instead of saving tokens. Each batch lives MONTH_DAYS, 30 days, and days_left counts it down from the day it landed. On day 25 the package month from day 0 has 5 days left, the start gift from day 3 has 8, and a top-up from day 12 has 17. The spend order is soonest burn first.", + "Every agent has its own bot and its own topic in one forum group. A person calls an agent by its tag, by @all, by a reply, or by writing in its topic. Another agent's bot calls it only by a tag or a reply: a bot never triggers the whole hive and never wakes a topic's owner by merely writing there. addressed in agent-hive-group.t27 is that rule, one line long.", + "Telegram gives no loop protection between bots, so two agents could answer each other forever. Two numbers stop it. MAX_BOT_DEPTH is 3: a person, then A, B and C, then silence. BOT_REPLY_COOLDOWN_MS is 5000: a bot answers another bot at most once in 5 s. may_answer always lets a person through, and a bot only inside the chain and after the cooldown.", + "cooled says whether 5000 ms have passed since the last bot message. /stop silences bot-to-bot turns in a chat for STOP_WINDOW_MS, 10 minutes, and a person is still heard: that is may_answer_after_stop. /mute keeps an agent quiet in a topic for MUTE_MS, an hour, but it still answers its own tag: heard_while_muted. Both are clocks, so nobody has to switch the hive back on.", + "Every night four checks of the card chat run: the thread, the status door, the merges and the parity. night_verdict sums them up: any fail is a fail, else anything unmeasured is unmeasured, else it is a pass. night_report turns that into one act: a fail opens an issue or comments on the open one, a pass closes an open issue or stays silent, and an unmeasured night leaves everything as it is.", + "The board at app.t27.ai/game/kanban draws every dispatch in one of 6 columns. column_of reads the claim a dispatch holds on its issue: running is running, accepted is done, rejected and awaiting review are in review, and failed is dropped. dispatch.t27 records one finding as it is: once an issue is closed and the list was read, every finished claim is drawn as done, whatever its verdict.", + "The game's comb has a cell for each issue, and before hive.t27 an accepted task, a lost lease and a cancel looked the same as no news. cell_effect gives each of the 10 event kinds its paint: a start white, an accepting review gold, a send-back amber, a lost lease red, a cancel grey. flash_alpha fades it over FLASH_MS, 1800 ms. A new cell rebuilds the scene, so cells_due places new cells in batches at most every 300 s." +]; +pub const LESSON_TASKS : [27]str = [ + "Set two sections missing and step through the tries; then mark the author told and see which column the issue goes to.", + "Make the two red paths green by changing only facts that could be true; then leave a section missing and watch the gate.", + "Show only failed tasks of every kind; then find which job state shows as dropped.", + "Find the day a labelled low issue moves up; then remove the label and drag to day 60.", + "Make a fifth issue critical and see where it lands; then label the unlabelled issue low.", + "Drag the queue to 0 and back to 1; then find the candidates that are admitted at any length.", + "Walk a task from none to accept; then make CI red at the ceiling and find the one way out.", + "Mark the bee as not heard from and see where it moves; then drag the time past 2400 ms.", + "Drag the wait from 0 to the cap and watch the bee's row change; then cut the platform drain to 1000 s.", + "Find the fewest keys of 1 lane that keep up with 58 an hour; then cut the memory to 1000 MB.", + "Find the wait count where the pause stops growing; then set 4 waits and find the second the row is due.", + "Let the running check conclude green; then make the ruleset unreadable and watch the discounted red.", + "Clear facts one by one from the start state and name each case you pass; then find a PR that is in no jam.", + "Find the case whose action is wait for the base; then drag to 200 and read the action.", + "Make every run cancelled; then move the failure before the merge base and look at the gate.", + "Compare 429 with codes 1308 and 1312; then read what no answer at all is.", + "Find the strike count where the rest stops growing; then switch to quota and read the rest.", + "Mark key 2 as tried and see which key gets the ring; then set three keys that said 1305.", + "Drag the keys from 0 to 100 and watch the credit meter; then find the second a probe may start.", + "Find the costs that give exactly 1 token; then find the monthly price that buys 36 reels.", + "Find the day the package month burns; then find the last day any batch is left.", + "Find the two ways only a person can call an agent; then turn on all four and compare the senders.", + "Find the gap at which agent B falls silent; then explain why C is silent too.", + "Drag the /stop clock past 10 minutes; then tag the muted agent.", + "Make the night pass with the issue open; then make one check unmeasured.", + "Open the issue and see where the four finished claims go; then say what a closed issue hides.", + "Tap the review cell and switch the verdict; then find the moment the flash is gone." +]; + +; --- Claims -------------------------------------------------------------------------------- +test the_course_is_three_cubed { + assert LESSONS_PER_MODULE == 3; + assert MODULE_COUNT == 9; + assert LESSON_COUNT == 27; + assert MODULE_COUNT * LESSONS_PER_MODULE == LESSON_COUNT; + assert LESSON_COUNT == 3 * 3 * 3; +} + +test it_starts_with_a_shaped_issue_and_ends_on_the_comb { + assert MODULE_IDS[0] == "the-task"; + assert LESSON_WIDGETS[0] == "hive-shape"; + assert LESSON_SPECS[0] == "specs/queen/shaping.t27"; + assert MODULE_IDS[8] == "night-and-the-board"; + assert LESSON_WIDGETS[26] == "hive-cell"; + assert LESSON_SPECS[26] == "specs/queen/hive.t27"; +} + +test the_order_comes_before_the_bee_and_the_bee_before_its_review { + assert MODULE_IDS[1] == "who-goes-next"; + assert LESSON_SPECS[3] == "specs/queen/priority.t27"; + assert MODULE_IDS[2] == "a-bees-life"; + assert LESSON_SPECS[6] == "specs/queen/dispatch.t27"; + assert MODULE_IDS[3] == "the-reviewer"; + assert LESSON_WIDGETS[11] == "hive-gate"; +} + +test the_jam_runs_the_review_jam_spec { + assert MODULE_IDS[4] == "the-jam"; + assert LESSON_SPECS[12] == "specs/automation/review-jam.t27"; + assert LESSON_SPECS[13] == "specs/automation/review-jam.t27"; + assert LESSON_WIDGETS[14] == "hive-master-red"; +} + +test keys_then_money_then_agents { + assert MODULE_IDS[5] == "keys"; + assert LESSON_SPECS[17] == "specs/queen/key_failover.t27"; + assert MODULE_IDS[6] == "credit-and-money"; + assert LESSON_SPECS[19] == "specs/automation/token-wallet.t27"; + assert MODULE_IDS[7] == "agents-together"; + assert LESSON_SPECS[22] == "specs/automation/agent-hive-group.t27"; +} + +test the_course_sends_nothing { + assert SENDS_NOTHING == true; + assert LOCALES[0] == "en"; + assert LOCALES[1] == "ru"; +} diff --git a/apps/website/public/learn/queen-and-hive/card.png b/apps/website/public/learn/queen-and-hive/card.png new file mode 100644 index 0000000000..1f59bc9430 Binary files /dev/null and b/apps/website/public/learn/queen-and-hive/card.png differ diff --git a/apps/website/public/learn/queen-and-hive/index.html b/apps/website/public/learn/queen-and-hive/index.html new file mode 100644 index 0000000000..5f7b37277b --- /dev/null +++ b/apps/website/public/learn/queen-and-hive/index.html @@ -0,0 +1,72 @@ + + + + + + +The Queen and the hive: how the t27 project builds itself · t27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+

Course

+

The Queen and the hive: how the t27 project builds itself

+

This project builds itself: an issue becomes a task for a bee, the bee writes a t27 spec, a reviewer bee checks it, it merges, and the site regenerates. The Queen who runs that loop is written as t27 specs with tests, and every widget here runs the fn bodies of one of them compiled to WebAssembly.

+

Course 11, 27 lessons, a live widget and a t27 spec each: shaping an issue, priority and review first, a task's life, the reviewer and the merge gate, the review jam, keys, credit and tokens, agents in one group, the night watch and the board. Every answer comes from a compiled spec.

+

Open the interactive course →

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/review-first/card.png b/apps/website/public/learn/review-first/card.png new file mode 100644 index 0000000000..a2f46613a5 Binary files /dev/null and b/apps/website/public/learn/review-first/card.png differ diff --git a/apps/website/public/learn/review-first/index.html b/apps/website/public/learn/review-first/index.html new file mode 100644 index 0000000000..f22c5d6733 --- /dev/null +++ b/apps/website/public/learn/review-first/index.html @@ -0,0 +1,250 @@ + + + + + + +Review first: the Queen and the hive course, lesson 6 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Review first

+

You will learn

Why the Queen closes the backlog while finished work waits for a review, and what still goes.

+

On 2026-10-18 the status page read 69 of 70 workers busy, and in one hour 102 dispatches, 56 finished and 11 reviewed. Bees started about nine for every verdict, and each new bee held files that the bees in review needed when they were sent back. review_first.t27 closes the backlog while the review queue is longer than REVIEW_QUEUE_MAX, 0. Work the review sent back and work the owner assigned still go: admission admits them at any queue length.

+

Try it

Drag the queue to 0 and back to 1; then find the candidates that are admitted at any length.

+

Open the interactive lesson →

+
Review first: no new work while finished work waits
Review first: no new work while finished work waits ↗
+

Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go.

+

specs/queen/review_first.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/review_first.t27 -- no new work from the backlog while finished work waits in review (gHashTag/t27#10555).
+; The owner's rule, 2026-10-18: the Queen takes nothing from the backlog while there is a queue in review.
+; Measured 2026-10-18 17:09Z on api.t27.ai/queen/status: 69 of 70 workers busy; in the last hour 102
+; dispatched, 56 finished, 11 reviewed; reviewable (the review sweep's own queue) 29, unreviewed 362.
+; The swarm starts about nine bees for every verdict, so the queue in review only grows, and each new bee
+; holds files (claim.t27) that the bees already in review need when they are sent back.
+; The rule:
+;   1. the review queue is exactly the review sweep's own filter, the one the status page counts as
+;      `reviewable`: the dispatch started, it finished, its review_state is NULL or 'wait', it was not
+;      reaped, and its issue is still open (in_review_queue);
+;   2. while that queue is longer than REVIEW_QUEUE_MAX (0: not while anything waits) a candidate from
+;      the backlog is refused, and the round says why (admission, R_REVIEW_FIRST);
+;   3. work the review itself asked for is never backlog: an issue the review sent back is dispatched
+;      again at any queue length (KIND_REWORK), and so is an issue the owner assigned by hand
+;      (KIND_ASSIGNED, the supervisor's pendingAssigns);
+;   4. the queue empties on its own: every outcome of the review that is not an accept has a clock
+;      (review_valve.t27), so a closed backlog always reopens without a person.
+; The host counts the queue and names each candidate's kind; this file decides. Not here: the wiring into
+; trios (queen-tick.ts candidate filter, vendored wasm and t27c gen-ts constants), how keys are shared
+; between bees and the reviewer, the review verdicts themselves. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewFirst;
+
+; --- 1. What a candidate is ---------------------------------------------------------------------------
+
+/// KIND_BACKLOG: an open issue with no dispatch the review is working on. KIND_REWORK: an issue whose
+/// last dispatch the review sent back (review_state 'sendBack'). KIND_ASSIGNED: an issue the owner
+/// assigned by hand. The codes are mirrored by number: a new kind takes the next code, none moves.
+pub const KIND_BACKLOG : u8 = 0;
+pub const KIND_REWORK : u8 = 1;
+pub const KIND_ASSIGNED : u8 = 2;
+
+/// The owner's hand wins over the review's send-back, and both win over the backlog.
+pub fn kind_of(assigned: bool, sent_back: bool) -> u8 {
+    if (assigned) { return KIND_ASSIGNED; }
+    if (sent_back) { return KIND_REWORK; }
+    return KIND_BACKLOG;
+}
+
+; --- 2. What the review queue is ----------------------------------------------------------------------
+
+/// A dispatch row's review_state as the host reads it: RS_NONE is NULL (no verdict yet), RS_WAIT is
+/// 'wait' (fewer verdicts than criteria, dispatch.t27 REVIEW_RULE), RS_OTHER is any written verdict.
+pub const RS_NONE : u8 = 0;
+pub const RS_WAIT : u8 = 1;
+pub const RS_OTHER : u8 = 2;
+
+/// Whether one dispatch row is in the review queue: the sweep's own filter (trios queen-tick.ts
+/// reviewFinishedDispatches, counted as `reviewable` by queen-public-status.ts). A row whose issue was
+/// closed is never reviewed, so it never holds the backlog shut.
+pub fn in_review_queue(started: bool, finished: bool, state: u8, reaped: bool, issue_open: bool) -> bool {
+    if (started == false) { return false; }
+    if (finished == false) { return false; }
+    if (reaped) { return false; }
+    if (issue_open == false) { return false; }
+    if (state == RS_NONE) { return true; }
+    return state == RS_WAIT;
+}
+
+; --- 3. Admission -------------------------------------------------------------------------------------
+
+/// How many dispatches may wait in review while the backlog stays open. Zero, the owner's words: not
+/// while anything is in review.
+pub const REVIEW_QUEUE_MAX : u32 = 0;
+
+/// R_ADMIT: the candidate may be chosen. R_REVIEW_FIRST: refused, the backlog is closed while the review
+/// queue is longer than REVIEW_QUEUE_MAX.
+pub const R_ADMIT : u8 = 0;
+pub const R_REVIEW_FIRST : u8 = 1;
+
+/// The refusal as the round reports it, after the queue length: "29 dispatches wait in review; ...".
+pub const REFUSAL_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty";
+
+pub fn backlog_open(review_queue: u32) -> bool {
+    return review_queue <= REVIEW_QUEUE_MAX;
+}
+
+pub fn admission(kind: u8, review_queue: u32) -> u8 {
+    if (kind == KIND_REWORK) { return R_ADMIT; }
+    if (kind == KIND_ASSIGNED) { return R_ADMIT; }
+    if (backlog_open(review_queue)) { return R_ADMIT; }
+    return R_REVIEW_FIRST;
+}
+
+pub fn admits(kind: u8, review_queue: u32) -> bool {
+    return admission(kind, review_queue) == R_ADMIT;
+}
+
+/// Whether a round that admitted no candidate owes the review-first sentence: some candidate was
+/// refused by this rule and none was admitted.
+pub fn round_refused(admitted: u32, refused: u32) -> bool {
+    if (admitted > 0) { return false; }
+    return refused > 0;
+}
+
+; --- 4. Tests -----------------------------------------------------------------------------------------
+
+test the_owner_hand_wins_then_the_review_then_the_backlog {
+    assert kind_of(false, false) == 0;
+    assert kind_of(false, true) == 1;
+    assert kind_of(true, false) == 2;
+    assert kind_of(true, true) == 2;
+}
+
+test the_queue_is_the_sweeps_own_filter {
+    assert in_review_queue(true, true, RS_NONE, false, true) == true;
+    assert in_review_queue(true, true, RS_WAIT, false, true) == true;
+    assert in_review_queue(true, true, RS_OTHER, false, true) == false;
+    assert in_review_queue(true, false, RS_NONE, false, true) == false;
+    assert in_review_queue(false, true, RS_NONE, false, true) == false;
+    assert in_review_queue(true, true, RS_NONE, true, true) == false;
+    assert in_review_queue(true, true, RS_NONE, false, false) == false;
+    assert in_review_queue(true, true, RS_WAIT, true, true) == false;
+    assert in_review_queue(true, true, 7, false, true) == false;
+}
+
+test the_backlog_is_closed_while_anything_waits_in_review {
+    assert admission(KIND_BACKLOG, 0) == 0;
+    assert admission(KIND_BACKLOG, 1) == 1;
+    assert admission(KIND_BACKLOG, 29) == 1;
+    assert admission(KIND_BACKLOG, 4294967295) == 1;
+    assert backlog_open(0) == true;
+    assert backlog_open(1) == false;
+    assert admits(KIND_BACKLOG, 0) == true;
+    assert admits(KIND_BACKLOG, 1) == false;
+}
+
+test work_the_review_asked_for_is_never_backlog {
+    assert admission(KIND_REWORK, 0) == 0;
+    assert admission(KIND_REWORK, 1) == 0;
+    assert admission(KIND_REWORK, 29) == 0;
+    assert admission(KIND_REWORK, 4294967295) == 0;
+    assert admits(KIND_REWORK, 362) == true;
+}
+
+test the_owners_assignment_is_never_held {
+    assert admission(KIND_ASSIGNED, 0) == 0;
+    assert admission(KIND_ASSIGNED, 29) == 0;
+    assert admits(KIND_ASSIGNED, 4294967295) == true;
+}
+
+test an_unknown_kind_is_backlog_and_is_held {
+    assert admission(3, 0) == 0;
+    assert admission(3, 1) == 1;
+    assert admission(255, 29) == 1;
+}
+
+test a_round_reports_the_rule_only_when_it_stopped_everything {
+    assert round_refused(0, 0) == false;
+    assert round_refused(0, 5) == true;
+    assert round_refused(1, 5) == false;
+    assert round_refused(3, 0) == false;
+}
+
+test the_measured_round_of_2026_10_18 {
+    assert admits(kind_of(false, false), 29) == false;
+    assert admits(kind_of(false, true), 29) == true;
+    assert admits(kind_of(true, false), 29) == true;
+    assert admits(kind_of(false, false), 0) == true;
+}
+
+; --- 5. Invariants ------------------------------------------------------------------------------------
+
+invariant the_codes_never_move {
+    assert KIND_BACKLOG == 0 and KIND_REWORK == 1 and KIND_ASSIGNED == 2;
+    assert RS_NONE == 0 and RS_WAIT == 1 and RS_OTHER == 2;
+    assert R_ADMIT == 0 and R_REVIEW_FIRST == 1;
+}
+
+/// The owner's bound: nothing may wait in review while the backlog is open.
+invariant the_bound_is_the_owners {
+    assert REVIEW_QUEUE_MAX == 0;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/shaping-an-issue/card.png b/apps/website/public/learn/shaping-an-issue/card.png new file mode 100644 index 0000000000..8d0b6f5d46 Binary files /dev/null and b/apps/website/public/learn/shaping-an-issue/card.png differ diff --git a/apps/website/public/learn/shaping-an-issue/index.html b/apps/website/public/learn/shaping-an-issue/index.html new file mode 100644 index 0000000000..db07b230fd --- /dev/null +++ b/apps/website/public/learn/shaping-an-issue/index.html @@ -0,0 +1,205 @@ + + + + + + +Shaping an issue: the Queen and the hive course, lesson 1 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Shaping an issue

+

You will learn

What the Queen does with an issue that lacks sections, and why she stops after two tries.

+

A bee can start only on an issue that says what to do and which files it may touch. The Queen holds every open issue to judge() of specs/policy/issue_shape.t27. One that is not ready she shapes: a model writes the missing sections, and the body goes back only if judge() then finds nothing missing. After SHAPE_ATTEMPT_LIMIT, 2 failed shapes, she stops and comments once with what is missing, and an epic is never shaped. shape_action in shaping.t27 decides, and a new try waits 360 minutes.

+

Try it

Set two sections missing and step through the tries; then mark the author told and see which column the issue goes to.

+

Open the interactive lesson →

+
Shaping: what the Queen does with an issue that is not ready
Shaping: what the Queen does with an issue that is not ready ↗
+

Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing.

+

specs/queen/shaping.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks
+; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix
+; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete.
+; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most
+; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as
+; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703,
+; #7642), which the rule counts as missing.
+; --
+; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section).
+; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a
+; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either
+; SHAPED by her (the missing sections are written by a model, every boundary path is checked against
+; the repository, and the body is written back only if judge() then returns 0), or, when shaping
+; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing.
+; --
+; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the
+; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is
+; accepted only when the issue itself cites it and its directory exists, and never when it would be a
+; new file in a language the only-t27 rule denies.
+; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenShaping;
+
+/// What the Queen does with one open issue.
+///   LEAVE  nothing: it is ready, a container, or already told
+///   SHAPE  ask for the missing sections, judge them, write them back if they pass
+///   TELL   comment once, naming what is missing
+pub const SH_LEAVE : u8 = 0;
+pub const SH_SHAPE : u8 = 1;
+pub const SH_TELL : u8 = 2;
+
+/// Attempts at shaping one issue before its author is told instead.
+pub const SHAPE_ATTEMPT_LIMIT : u32 = 2;
+/// Issues shaped per round. A shape is one model call; the round has other work.
+pub const SHAPES_PER_ROUND : u32 = 3;
+/// Minutes between two attempts on one issue, so a refused shape is not retried every round.
+pub const SHAPE_RETRY_MINUTES : u32 = 360;
+/// A boundary is a reservation; one naming more files than this is not a task, it is a project.
+pub const MAX_BOUNDARY_PATHS : u32 = 12;
+
+/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap;
+/// `attempts`: shapes tried so far; `told`: the author was already told.
+pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 {
+    if (missing == 0) { return SH_LEAVE; }
+    if (container) { return SH_LEAVE; }
+    if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; }
+    if (told) { return SH_LEAVE; }
+    return SH_TELL;
+}
+
+/// Is one proposed boundary path acceptable?
+///   exists       the file is in the repository at the base ref
+///   dir_exists   its directory is (a new file beside its siblings)
+///   cited        the issue's own title or body names this path
+///   foreign_new  it would be a NEW file whose extension the only-t27 rule denies to new work
+/// A file that exists is always acceptable: finding the file a title describes is the point. A file
+/// that does not exist must be cited by the issue, in a directory that exists, and not foreign.
+pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool {
+    if (exists) { return true; }
+    if (dir_exists == false) { return false; }
+    if (foreign_new) { return false; }
+    return cited;
+}
+
+/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and
+/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted.
+pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool {
+    if (missing_after != 0) { return false; }
+    if (paths == 0) { return false; }
+    if (paths > MAX_BOUNDARY_PATHS) { return false; }
+    return rejected == 0;
+}
+
+/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue.
+pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool {
+    if (attempts == 0) { return true; }
+    return minutes_since_last >= SHAPE_RETRY_MINUTES;
+}
+
+test a_ready_issue_and_a_container_are_left_alone {
+    assert shape_action(0, false, 0, false) == SH_LEAVE;
+    assert shape_action(15, true, 0, false) == SH_LEAVE;
+    assert shape_action(1, true, 9, false) == SH_LEAVE;
+}
+
+test an_unready_issue_is_shaped_then_its_author_is_told_once {
+    assert shape_action(1, false, 0, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE;
+    assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE;
+}
+
+test a_boundary_path_must_be_true_of_the_repository {
+    // an existing file, cited or found
+    assert path_accepted(true, true, false, false) == true;
+    assert path_accepted(true, true, true, false) == true;
+    // a new spec the issue asks for, beside its siblings
+    assert path_accepted(false, true, true, false) == true;
+    // a new file the issue never mentioned: invented
+    assert path_accepted(false, true, false, false) == false;
+    // a new file in a directory that does not exist
+    assert path_accepted(false, false, true, false) == false;
+    // a new .py or .ts the only-t27 rule denies, even when cited
+    assert path_accepted(false, true, true, true) == false;
+}
+
+test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds {
+    assert shape_lands(0, 1, 0) == true;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true;
+    assert shape_lands(1, 3, 0) == false;
+    assert shape_lands(0, 0, 0) == false;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false;
+    assert shape_lands(0, 3, 1) == false;
+}
+
+test a_refused_shape_is_not_retried_every_round {
+    assert attempt_due(0, 0) == true;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES) == true;
+}
+
+invariant the_codes_and_limits_never_move {
+    assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2;
+    assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0;
+    assert MAX_BOUNDARY_PATHS > 0;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/sitemap.xml b/apps/website/public/learn/sitemap.xml index c00d74bcf6..574263dae4 100644 --- a/apps/website/public/learn/sitemap.xml +++ b/apps/website/public/learn/sitemap.xml @@ -561,4 +561,60 @@ https://t27.ai/ru/learn/positional-encoding/ https://t27.ai/ru/learn/an-encoder-block/ https://t27.ai/ru/learn/ppo-and-the-reward/ + https://t27.ai/learn/queen-and-hive/ + https://t27.ai/learn/shaping-an-issue/ + https://t27.ai/learn/a-boundary-of-real-files/ + https://t27.ai/learn/one-task-three-kinds/ + https://t27.ai/learn/aging-a-label/ + https://t27.ai/learn/the-queue/ + https://t27.ai/learn/review-first/ + https://t27.ai/learn/a-tasks-life/ + https://t27.ai/learn/the-ship-flies-home/ + https://t27.ai/learn/a-deploy-mid-flight/ + https://t27.ai/learn/enough-reviewers/ + https://t27.ai/learn/back-off/ + https://t27.ai/learn/the-merge-gate/ + https://t27.ai/learn/the-review-jam/ + https://t27.ai/learn/the-queens-move/ + https://t27.ai/learn/inherited-red/ + https://t27.ai/learn/a-refused-key/ + https://t27.ai/learn/a-key-rests/ + https://t27.ai/learn/the-next-key/ + https://t27.ai/learn/no-credit-for-keys/ + https://t27.ai/learn/twice-the-cost/ + https://t27.ai/learn/tokens-that-burn/ + https://t27.ai/learn/who-is-called/ + https://t27.ai/learn/three-bots-deep/ + https://t27.ai/learn/stop-and-mute/ + https://t27.ai/learn/the-night-watch/ + https://t27.ai/learn/the-public-board/ + https://t27.ai/learn/the-comb-answers/ + https://t27.ai/ru/learn/queen-and-hive/ + https://t27.ai/ru/learn/shaping-an-issue/ + https://t27.ai/ru/learn/a-boundary-of-real-files/ + https://t27.ai/ru/learn/one-task-three-kinds/ + https://t27.ai/ru/learn/aging-a-label/ + https://t27.ai/ru/learn/the-queue/ + https://t27.ai/ru/learn/review-first/ + https://t27.ai/ru/learn/a-tasks-life/ + https://t27.ai/ru/learn/the-ship-flies-home/ + https://t27.ai/ru/learn/a-deploy-mid-flight/ + https://t27.ai/ru/learn/enough-reviewers/ + https://t27.ai/ru/learn/back-off/ + https://t27.ai/ru/learn/the-merge-gate/ + https://t27.ai/ru/learn/the-review-jam/ + https://t27.ai/ru/learn/the-queens-move/ + https://t27.ai/ru/learn/inherited-red/ + https://t27.ai/ru/learn/a-refused-key/ + https://t27.ai/ru/learn/a-key-rests/ + https://t27.ai/ru/learn/the-next-key/ + https://t27.ai/ru/learn/no-credit-for-keys/ + https://t27.ai/ru/learn/twice-the-cost/ + https://t27.ai/ru/learn/tokens-that-burn/ + https://t27.ai/ru/learn/who-is-called/ + https://t27.ai/ru/learn/three-bots-deep/ + https://t27.ai/ru/learn/stop-and-mute/ + https://t27.ai/ru/learn/the-night-watch/ + https://t27.ai/ru/learn/the-public-board/ + https://t27.ai/ru/learn/the-comb-answers/ diff --git a/apps/website/public/learn/stop-and-mute/card.png b/apps/website/public/learn/stop-and-mute/card.png new file mode 100644 index 0000000000..c72044c19d Binary files /dev/null and b/apps/website/public/learn/stop-and-mute/card.png differ diff --git a/apps/website/public/learn/stop-and-mute/index.html b/apps/website/public/learn/stop-and-mute/index.html new file mode 100644 index 0000000000..c6593d3fce --- /dev/null +++ b/apps/website/public/learn/stop-and-mute/index.html @@ -0,0 +1,436 @@ + + + + + + +Stop and mute: the Queen and the hive course, lesson 24 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Stop and mute

+

You will learn

What /stop and /mute do, and why neither can lose a person or a direct call.

+

cooled says whether 5000 ms have passed since the last bot message. /stop silences bot-to-bot turns in a chat for STOP_WINDOW_MS, 10 minutes, and a person is still heard: that is may_answer_after_stop. /mute keeps an agent quiet in a topic for MUTE_MS, an hour, but it still answers its own tag: heard_while_muted. Both are clocks, so nobody has to switch the hive back on.

+

Try it

Drag the /stop clock past 10 minutes; then tag the muted agent.

+

Open the interactive lesson →

+
The cooldown clock, /stop and /mute
The cooldown clock, /stop and /mute ↗
+

One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-comb-answers/card.png b/apps/website/public/learn/the-comb-answers/card.png new file mode 100644 index 0000000000..656b2ed8ae Binary files /dev/null and b/apps/website/public/learn/the-comb-answers/card.png differ diff --git a/apps/website/public/learn/the-comb-answers/index.html b/apps/website/public/learn/the-comb-answers/index.html new file mode 100644 index 0000000000..529394bc39 --- /dev/null +++ b/apps/website/public/learn/the-comb-answers/index.html @@ -0,0 +1,259 @@ + + + + + + +The comb answers: the Queen and the hive course, lesson 27 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The comb answers

+

You will learn

How the game shows that something happened to a task, and how often the map grows.

+

The game's comb has a cell for each issue, and before hive.t27 an accepted task, a lost lease and a cancel looked the same as no news. cell_effect gives each of the 10 event kinds its paint: a start white, an accepting review gold, a send-back amber, a lost lease red, a cancel grey. flash_alpha fades it over FLASH_MS, 1800 ms. A new cell rebuilds the scene, so cells_due places new cells in batches at most every 300 s.

+

Try it

Tap the review cell and switch the verdict; then find the moment the flash is gone.

+

Open the interactive lesson →

+
The comb answers: what an event does to a cell
The comb answers: what an event does to a cell ↗
+

Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells.

+

specs/queen/hive.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to
+; a cell, how a ship leaves, and how often the map may grow
+; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game
+; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4.
+; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day):
+; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen.
+; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far
+;   from the repository it works for.
+; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole
+;   scene: one rebuild for most bees that started.
+; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same
+;   as no news.
+; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two
+; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenHive;
+
+pub const HIVE_KINDS : u8 = 10;
+
+; --- 1. Where a ship flies ---------------------------------------------------------------------------
+
+/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this
+/// map can show (a repository with no region, or a task that is not an issue).
+pub const CELL_PLACED : u8 = 0;
+pub const CELL_PENDING : u8 = 1;
+pub const CELL_NONE : u8 = 2;
+
+/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim,
+/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is
+/// placed. HOME: flying back to the entrance, its task over.
+pub const SM_PARK : u8 = 0;
+pub const SM_ORBIT : u8 = 1;
+pub const SM_HOVER : u8 = 2;
+pub const SM_PORTAL : u8 = 3;
+pub const SM_HOME : u8 = 4;
+
+/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no
+/// longer in the feed.
+pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 {
+    if (departing) { return SM_HOME; }
+    if (queued) { return SM_PARK; }
+    if (cell == CELL_PENDING) { return SM_PORTAL; }
+    if (cell != CELL_PLACED) { return SM_PARK; }
+    if (working) { return SM_ORBIT; }
+    return SM_HOVER;
+}
+
+; --- 2. How a ship leaves ----------------------------------------------------------------------------
+
+/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing.
+pub const DEPART_MS : u32 = 2400;
+
+pub fn ship_kept(ms_since_left: u32) -> bool {
+    return ms_since_left < DEPART_MS;
+}
+
+/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing.
+pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 {
+    if (departing == false) { return 1000; }
+    if (ms_since_left >= DEPART_MS) { return 0; }
+    return 1000 - (ms_since_left * 1000) / DEPART_MS;
+}
+
+; --- 3. What an event does to a cell -----------------------------------------------------------------
+
+/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the
+/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is
+/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it.
+pub const FX_NONE : u8 = 0;
+pub const FX_START : u8 = 1;
+pub const FX_ACCEPT : u8 = 2;
+pub const FX_SEND_BACK : u8 = 3;
+pub const FX_LOST : u8 = 4;
+pub const FX_CANCEL : u8 = 5;
+
+/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by
+/// its verdict, cancel CANCEL; everything else nothing.
+pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0];
+
+/// `accepted`: a review's verdict was an acceptance. Only a review reads it.
+pub fn cell_effect(kind: u8, accepted: bool) -> u8 {
+    if (kind >= HIVE_KINDS) { return FX_NONE; }
+    let fx : u8 = FX_OF_KIND[kind];
+    if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; }
+    return fx;
+}
+
+/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn.
+pub const FLASH_MS : u32 = 1800;
+
+pub fn flash_alpha(ms_since: u32) -> u32 {
+    if (ms_since >= FLASH_MS) { return 0; }
+    return 1000 - (ms_since * 1000) / FLASH_MS;
+}
+
+; --- 4. How often the map may grow -------------------------------------------------------------------
+
+/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding
+/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS --
+/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of
+/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's
+/// portal (SM_PORTAL), so where it is going is already true.
+pub const CELL_BATCH_SECONDS : u32 = 300;
+
+pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool {
+    if (pending == 0) { return false; }
+    if (placed_before == false) { return true; }
+    return seconds_since_batch >= CELL_BATCH_SECONDS;
+}
+
+test a_ship_flies_where_its_bee_is {
+    // queued: at the entrance, wherever its cell stands
+    assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK;
+    assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK;
+    // claimed, its cell placed: orbit while heard, hover when quiet
+    assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT;
+    assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER;
+    // its cell not placed yet: at its repository's portal, working or not
+    assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL;
+    assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL;
+    // no cell this map can show: back at the entrance
+    assert ship_mode(false, true, CELL_NONE, false) == SM_PARK;
+    // gone from the feed: home, whatever it was
+    assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME;
+    assert ship_mode(true, false, CELL_NONE, true) == SM_HOME;
+}
+
+test a_ship_leaves_by_flying_home {
+    assert ship_kept(0) == true;
+    assert ship_kept(DEPART_MS - 1) == true;
+    assert ship_kept(DEPART_MS) == false;
+    assert ship_alpha(false, 0) == 1000;
+    assert ship_alpha(false, 99999) == 1000;
+    assert ship_alpha(true, 0) == 1000;
+    assert ship_alpha(true, 1200) == 500;
+    assert ship_alpha(true, DEPART_MS - 1) == 1;
+    assert ship_alpha(true, DEPART_MS) == 0;
+    assert ship_alpha(true, 4000000000) == 0;
+}
+
+test an_event_lights_its_cell {
+    assert cell_effect(0, false) == FX_START;
+    assert cell_effect(1, true) == FX_NONE;
+    assert cell_effect(2, false) == FX_NONE;
+    assert cell_effect(3, false) == FX_LOST;
+    assert cell_effect(4, true) == FX_NONE;
+    assert cell_effect(5, true) == FX_ACCEPT;
+    assert cell_effect(5, false) == FX_SEND_BACK;
+    assert cell_effect(6, true) == FX_NONE;
+    assert cell_effect(7, false) == FX_CANCEL;
+    assert cell_effect(8, false) == FX_NONE;
+    assert cell_effect(9, true) == FX_NONE;
+    // only a review reads the verdict
+    assert cell_effect(3, true) == FX_LOST;
+    assert cell_effect(HIVE_KINDS, true) == FX_NONE;
+}
+
+test a_flash_fades {
+    assert flash_alpha(0) == 1000;
+    assert flash_alpha(900) == 500;
+    assert flash_alpha(FLASH_MS - 1) == 1;
+    assert flash_alpha(FLASH_MS) == 0;
+    assert flash_alpha(4000000000) == 0;
+}
+
+test the_map_grows_in_batches {
+    assert cells_due(0, false, 0) == false;
+    assert cells_due(0, true, 99999) == false;
+    // the first answer places every cell at once
+    assert cells_due(3, false, 0) == true;
+    // after that, one batch a period
+    assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false;
+    assert cells_due(1, true, CELL_BATCH_SECONDS) == true;
+}
+
+invariant one_row_per_kind {
+    assert HIVE_KINDS == 10;
+    assert FX_OF_KIND[5] == FX_ACCEPT;
+    assert FLASH_MS < DEPART_MS * 2;
+    assert CELL_BATCH_SECONDS == 300;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-merge-gate/card.png b/apps/website/public/learn/the-merge-gate/card.png new file mode 100644 index 0000000000..670e1177b5 Binary files /dev/null and b/apps/website/public/learn/the-merge-gate/card.png differ diff --git a/apps/website/public/learn/the-merge-gate/index.html b/apps/website/public/learn/the-merge-gate/index.html new file mode 100644 index 0000000000..89e9554f9d --- /dev/null +++ b/apps/website/public/learn/the-merge-gate/index.html @@ -0,0 +1,377 @@ + + + + + + +The merge gate: the Queen and the hive course, lesson 12 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The merge gate

+

You will learn

When the merger may merge a pull request a reviewer bee approved.

+

The reviewer bee approves, and the merger merges; merger_gate.t27 is the rule between them. check_passes lets a green check through and blocks any check still running. A red one passes only when it is not required, has concluded, and the bee named it as discounted, and with the ruleset unreadable every check counts as required. gate_open needs at least 1 posted check, 0 running and 0 blocking: zero checks is not zero failures.

+

Try it

Let the running check conclude green; then make the ruleset unreadable and watch the discounted red.

+

Open the interactive lesson →

+
The merge gate: when an approved pull request may merge
The merge gate: when an approved pull request may merge ↗
+

Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks.

+

specs/queen/merger_gate.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved
+; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq).
+; It does not import this spec, and t27c gen-js lowers no function bodies (#7300).
+; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario
+; asserts the verdict that scenario pins for the workflow.
+; Negative controls: discounted required check, unconcluded check, required check never posted,
+; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run
+; of the same check was CANCELLED (#7317)
+; ASCII only (L3)
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenMergerGate;
+
+/// With the ruleset unreadable the merger cannot tell which checks are required,
+/// so every check counts as required and no red one is discountable (#5776 item 2).
+pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool {
+    return !ruleset_readable || required;
+}
+
+/// A red check passes only when it is not required, has concluded, and is discounted
+pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool {
+    return !required && concluded && discounted;
+}
+
+/// A required check passes when it is posted and green
+pub fn required_check_passes(posted: bool, green: bool) -> bool {
+    return posted && green;
+}
+
+/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks;
+/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check
+/// that does not count as required.
+pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool {
+    if (!concluded) { return false; }
+    if (green) { return true; }
+    return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted);
+}
+
+// ---------------------------------------------------------------------------
+// What master says about a discounted red (#7317)
+//
+// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its
+// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled
+// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it.
+// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the
+// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or
+// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts.
+
+/// One master run of the red check, as the merger reads its job's status and conclusion.
+/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code.
+pub const RUN_MISSING : u8 = 0;
+pub const RUN_FAILURE : u8 = 1;
+pub const RUN_SUCCESS : u8 = 2;
+pub const RUN_CANCELLED : u8 = 3;
+pub const RUN_SKIPPED : u8 = 4;
+pub const RUN_TIMED_OUT : u8 = 5;
+/// queued or in progress
+pub const RUN_RUNNING : u8 = 6;
+/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail
+pub const RUN_OTHER : u8 = 7;
+
+/// What master says about the PR's red.
+pub const MASTER_UNKNOWN : u8 = 0;
+/// a completed failure: the red is inherited
+pub const MASTER_RED : u8 = 1;
+/// a completed success: master was green, the red is the PR's own
+pub const MASTER_GREEN : u8 = 2;
+
+/// One run on its own. Only a completed failure or a completed success decides anything.
+pub fn run_says(run: u8) -> u8 {
+    if (run == RUN_FAILURE) { return MASTER_RED; }
+    if (run == RUN_SUCCESS) { return MASTER_GREEN; }
+    return MASTER_UNKNOWN;
+}
+
+/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN.
+/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not.
+/// A run older than the PR's merge base is not evidence about the code the PR started from.
+pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 {
+    if (so_far != MASTER_UNKNOWN) { return so_far; }
+    if (!at_or_after_merge_base) { return MASTER_UNKNOWN; }
+    return run_says(run);
+}
+
+/// A red is discounted only when the bee's approval names it AND master says red.
+/// This is the `discounted` that check_passes takes.
+pub fn discount_holds(bee_named: bool, master: u8) -> bool {
+    return bee_named && master == MASTER_RED;
+}
+
+/// The gate opens when at least one check has posted, none is still running,
+/// and none blocks. Zero checks is not zero failures.
+pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool {
+    return posted > 0 && running == 0 && blocking == 0;
+}
+
+// Tests for counts_as_required
+
+test counts_as_required_follows_the_ruleset_when_readable {
+    assert counts_as_required(true, true) == true;
+    assert counts_as_required(true, false) == false;
+}
+
+test counts_as_required_is_every_check_when_unreadable {
+    assert counts_as_required(false, false) == true;
+    assert counts_as_required(false, true) == true;
+}
+
+// Tests for red_check_passes
+
+// Positive case: non-required check that concluded and is discounted
+test red_check_passes_valid_discounted_non_required {
+    assert red_check_passes(false, true, true) == true;
+}
+
+// Negative control: discounted required check (required=true, cannot be discounted)
+test red_check_passes_discounted_required_fails {
+    assert red_check_passes(true, true, true) == false;
+}
+
+// Negative control: unconcluded check (concluded=false, even if discounted)
+test red_check_passes_unconcluded_fails {
+    assert red_check_passes(false, false, true) == false;
+}
+
+// Negative control: non-required check that concluded but not discounted
+test red_check_passes_non_discounted_fails {
+    assert red_check_passes(false, true, false) == false;
+}
+
+test red_check_passes_all_false_fails {
+    assert red_check_passes(true, false, false) == false;
+}
+
+// Tests for required_check_passes
+
+test required_check_passes_posted_and_green_passes {
+    assert required_check_passes(true, true) == true;
+}
+
+// Self-test scenario "required check never posted"
+test required_check_passes_not_posted_fails {
+    assert required_check_passes(false, true) == false;
+}
+
+test required_check_passes_posted_but_not_green_fails {
+    assert required_check_passes(true, false) == false;
+}
+
+test required_check_passes_neither_posted_nor_green_fails {
+    assert required_check_passes(false, false) == false;
+}
+
+// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted)
+
+test check_passes_green_advisory_and_green_required_pass {
+    assert check_passes(true, false, true, true, false) == true;
+    assert check_passes(true, true, true, true, false) == true;
+}
+
+// Self-test scenario "red advisory check, discounted in the bee's approval"
+test check_passes_red_advisory_discounted_passes {
+    assert check_passes(true, false, true, false, true) == true;
+}
+
+// Self-test scenario "red advisory check, not discounted"
+test check_passes_red_advisory_not_discounted_blocks {
+    assert check_passes(true, false, true, false, false) == false;
+}
+
+// Self-test scenario "red REQUIRED check, discounted anyway"
+test check_passes_red_required_discounted_blocks {
+    assert check_passes(true, true, true, false, true) == false;
+}
+
+// Self-test scenarios "a check still running" and "pending commit status blocks":
+// not concluded blocks whatever else is true, a discount included
+test check_passes_running_check_blocks {
+    assert check_passes(true, false, false, true, true) == false;
+    assert check_passes(true, false, false, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable: a discounted red check still blocks"
+test check_passes_unreadable_ruleset_discounted_red_blocks {
+    assert check_passes(false, false, true, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable, every check green"
+test check_passes_unreadable_ruleset_green_passes {
+    assert check_passes(false, false, true, true, false) == true;
+}
+
+// Tests for gate_open: (posted, running, blocking)
+
+// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks
+test gate_open_posted_checks_all_pass_opens {
+    assert gate_open(2, 0, 0) == true;
+    assert gate_open(1, 0, 0) == true;
+}
+
+// Negative control: zero checks is not zero failures
+test gate_open_zero_checks_stays_closed {
+    assert gate_open(0, 0, 0) == false;
+}
+
+// Self-test scenario "a check still running"
+test gate_open_running_check_keeps_it_closed {
+    assert gate_open(3, 1, 0) == false;
+}
+
+// Self-test scenario "two red checks, only one discounted"
+test gate_open_one_blocking_check_keeps_it_closed {
+    assert gate_open(4, 0, 1) == false;
+}
+
+// Tests for run_says, look_back and discount_holds (#7317)
+
+test only_a_completed_failure_says_red {
+    assert run_says(RUN_FAILURE) == MASTER_RED;
+    assert run_says(RUN_SUCCESS) == MASTER_GREEN;
+}
+
+test every_other_run_says_unknown {
+    assert run_says(RUN_MISSING) == MASTER_UNKNOWN;
+    assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN;
+    assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN;
+    assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN;
+    assert run_says(RUN_RUNNING) == MASTER_UNKNOWN;
+    assert run_says(RUN_OTHER) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master newest cancelled, older failure at or after the merge base"
+test look_back_passes_a_cancelled_run_to_an_older_failure {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED;
+}
+
+// Self-test scenario "master failure only before the merge base"
+test look_back_stops_at_the_merge_base {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN;
+    assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN;
+}
+
+// The newest decisive run wins, in both directions
+test look_back_keeps_the_newest_decisive_run {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN;
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED;
+    assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED;
+}
+
+test look_back_over_only_unknown_runs_stays_unknown {
+    assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master failure": discounted
+test discount_holds_on_a_master_failure {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true;
+}
+
+// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked
+test pr_red_while_master_cancelled_is_blocked {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false;
+}
+
+// Self-test scenario "master success": the red is the PR's own -> blocked
+test pr_red_while_master_green_is_blocked {
+    assert discount_holds(true, MASTER_GREEN) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false;
+}
+
+// Master red, but the bee never named the check: blocked
+test master_red_without_the_bees_line_is_blocked {
+    assert discount_holds(false, MASTER_RED) == false;
+    assert discount_holds(false, MASTER_UNKNOWN) == false;
+}
+
+invariant the_run_and_master_codes_never_move {
+    assert RUN_MISSING == 0;
+    assert RUN_FAILURE == 1;
+    assert RUN_SUCCESS == 2;
+    assert RUN_CANCELLED == 3;
+    assert RUN_SKIPPED == 4;
+    assert RUN_TIMED_OUT == 5;
+    assert RUN_RUNNING == 6;
+    assert RUN_OTHER == 7;
+    assert MASTER_UNKNOWN == 0;
+    assert MASTER_RED == 1;
+    assert MASTER_GREEN == 2;
+}
+
+invariant unknown_never_discounts {
+    assert discount_holds(true, MASTER_UNKNOWN) == false;
+    assert discount_holds(true, run_says(RUN_MISSING)) == false;
+    assert discount_holds(true, run_says(RUN_CANCELLED)) == false;
+    assert discount_holds(true, run_says(RUN_SKIPPED)) == false;
+    assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false;
+    assert discount_holds(true, run_says(RUN_RUNNING)) == false;
+    assert discount_holds(true, run_says(RUN_OTHER)) == false;
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-next-key/card.png b/apps/website/public/learn/the-next-key/card.png new file mode 100644 index 0000000000..5faef35617 Binary files /dev/null and b/apps/website/public/learn/the-next-key/card.png differ diff --git a/apps/website/public/learn/the-next-key/index.html b/apps/website/public/learn/the-next-key/index.html new file mode 100644 index 0000000000..d4b63f0b88 --- /dev/null +++ b/apps/website/public/learn/the-next-key/index.html @@ -0,0 +1,458 @@ + + + + + + +The next key: the Queen and the hive course, lesson 18 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The next key

+

You will learn

Which key a bee tries next, and when it moves to another pool or gives up.

+

On 2026-10-17 a bee started on key 9 of 10 and died 7 s later while 33 other keys stood by: it got one key at dispatch and kept it. next_free now picks the next key after the last one that is neither tried nor resting. 1305 is an overload of the pool's model, so once POOL_OVERLOAD_KEYS, 3, keys said it within a minute, step moves the bee to the next pool. After 8 attempts the turn ends.

+

Try it

Mark key 2 as tried and see which key gets the ring; then set three keys that said 1305.

+

Open the interactive lesson →

+
The next key: a bee moves on instead of dying
The next key: a bee moves on instead of dying ↗
+

A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-night-watch/card.png b/apps/website/public/learn/the-night-watch/card.png new file mode 100644 index 0000000000..53cd862fb2 Binary files /dev/null and b/apps/website/public/learn/the-night-watch/card.png differ diff --git a/apps/website/public/learn/the-night-watch/index.html b/apps/website/public/learn/the-night-watch/index.html new file mode 100644 index 0000000000..62c1077f6c --- /dev/null +++ b/apps/website/public/learn/the-night-watch/index.html @@ -0,0 +1,220 @@ + + + + + + +The night watch: the Queen and the hive course, lesson 25 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The night watch

+

You will learn

What the night watch does in the morning with four card checks.

+

Every night four checks of the card chat run: the thread, the status door, the merges and the parity. night_verdict sums them up: any fail is a fail, else anything unmeasured is unmeasured, else it is a pass. night_report turns that into one act: a fail opens an issue or comments on the open one, a pass closes an open issue or stays silent, and an unmeasured night leaves everything as it is.

+

Try it

Make the night pass with the issue open; then make one check unmeasured.

+

Open the interactive lesson →

+
The night watch: four checks, one verdict, one report
The night watch: four checks, one verdict, one report ↗
+

Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent.

+

specs/automation/card-chat-night-watch.t27

// SPDX-License-Identifier: Apache-2.0
+// Card Chat Night Watch - verdict and reporting logic
+// Part of EPIC #5388
+
+module CardChatNightWatch {
+    // Verdict constants
+    const CHECK_PASS: u8 = 0
+    const CHECK_FAIL: u8 = 1
+    const CHECK_UNMEASURED: u8 = 2
+
+    // Report constants
+    const REPORT_SILENT: u8 = 0
+    const REPORT_OPEN_ISSUE: u8 = 1
+    const REPORT_COMMENT: u8 = 2
+    const REPORT_CLOSE_ISSUE: u8 = 3
+    const REPORT_LEAVE_AS_IS: u8 = 4
+
+    // Total number of card check verdicts (thread, status door, merges, parity)
+    const CARD_CHECK_VERDICTS: u8 = 4
+
+    // Determine night verdict based on failures and unmeasured checks
+    // Any fail -> FAIL; else any unmeasured -> UNMEASURED; else PASS
+    fn night_verdict(fails: u8, unmeasured: u8) -> u8 {
+        if fails > 0 {
+            return CHECK_FAIL
+        } else if unmeasured > 0 {
+            return CHECK_UNMEASURED
+        } else {
+            return CHECK_PASS
+        }
+    }
+
+    // Determine night report based on verdict and issue state
+    // FAIL and no open issue -> OPEN_ISSUE
+    // FAIL and an open issue -> COMMENT
+    // PASS and an open issue -> CLOSE_ISSUE
+    // PASS and none -> SILENT
+    // UNMEASURED -> LEAVE_AS_IS whatever the issue state
+    // a verdict above 2 -> LEAVE_AS_IS
+    fn night_report(verdict: u8, issue_open: bool) -> u8 {
+        if verdict > 2 {
+            return REPORT_LEAVE_AS_IS
+        }
+        
+        if verdict == CHECK_FAIL {
+            if issue_open {
+                return REPORT_COMMENT
+            } else {
+                return REPORT_OPEN_ISSUE
+            }
+        } else if verdict == CHECK_PASS {
+            if issue_open {
+                return REPORT_CLOSE_ISSUE
+            } else {
+                return REPORT_SILENT
+            }
+        } else {
+            return REPORT_LEAVE_AS_IS
+        }
+    }
+
+    // Check if night counts are acceptable
+    // true when fails + unmeasured <= CARD_CHECK_VERDICTS
+    fn night_counts_ok(fails: u8, unmeasured: u8) -> bool {
+        let total = fails + unmeasured
+        return total <= CARD_CHECK_VERDICTS
+    }
+
+    // Tests
+    test night_verdict_all_pass
+        let result = night_verdict(0, 0)
+        assert result == CHECK_PASS
+
+    test night_verdict_any_fail
+        let result = night_verdict(1, 0)
+        assert result == CHECK_FAIL
+
+    test night_verdict_fail_beside_unmeasured
+        let result = night_verdict(1, 3)
+        assert result == CHECK_FAIL
+
+    test night_verdict_any_unmeasured_no_fail
+        let result = night_verdict(0, 1)
+        assert result == CHECK_UNMEASURED
+
+    test night_report_fail_no_issue
+        let result = night_report(CHECK_FAIL, false)
+        assert result == REPORT_OPEN_ISSUE
+
+    test night_report_fail_with_issue
+        let result = night_report(CHECK_FAIL, true)
+        assert result == REPORT_COMMENT
+
+    test night_report_pass_with_issue
+        let result = night_report(CHECK_PASS, true)
+        assert result == REPORT_CLOSE_ISSUE
+
+    test night_report_pass_no_issue
+        let result = night_report(CHECK_PASS, false)
+        assert result == REPORT_SILENT
+
+    test night_report_unmeasured_with_issue
+        let result = night_report(CHECK_UNMEASURED, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_report_unmeasured_no_issue
+        let result = night_report(CHECK_UNMEASURED, false)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_report_invalid_verdict
+        let result = night_report(5, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_counts_ok_within_limit
+        let result = night_counts_ok(2, 2)
+        assert result == true
+
+    test night_counts_ok_at_limit
+        let result = night_counts_ok(4, 0)
+        assert result == true
+
+    test night_counts_ok_over_limit
+        let result = night_counts_ok(3, 2)
+        assert result == false
+
+    test night_counts_ok_zero
+        let result = night_counts_ok(0, 0)
+        assert result == true
+
+    // User scenario tests
+    test scenario_unmeasured_night_with_open_issue
+        // Given a night where the render answered 401 to every read
+        // when night_report(night_verdict(0, 4), true) is evaluated
+        // then it returns REPORT_LEAVE_AS_IS
+        let verdict = night_verdict(0, 4)
+        let result = night_report(verdict, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test scenario_fail_night_no_open_issue
+        // Given a night where the parity check failed and nothing else did
+        // when night_report(night_verdict(1, 0), false) is evaluated
+        // then it returns REPORT_OPEN_ISSUE
+        let verdict = night_verdict(1, 0)
+        let result = night_report(verdict, false)
+        assert result == REPORT_OPEN_ISSUE
+}

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-public-board/card.png b/apps/website/public/learn/the-public-board/card.png new file mode 100644 index 0000000000..e5c8ab7967 Binary files /dev/null and b/apps/website/public/learn/the-public-board/card.png differ diff --git a/apps/website/public/learn/the-public-board/index.html b/apps/website/public/learn/the-public-board/index.html new file mode 100644 index 0000000000..a0cae99082 --- /dev/null +++ b/apps/website/public/learn/the-public-board/index.html @@ -0,0 +1,669 @@ + + + + + + +The public board: the Queen and the hive course, lesson 26 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The public board

+

You will learn

Which column the public board draws each claim in, and the case it gets wrong.

+

The board at app.t27.ai/game/kanban draws every dispatch in one of 6 columns. column_of reads the claim a dispatch holds on its issue: running is running, accepted is done, rejected and awaiting review are in review, and failed is dropped. dispatch.t27 records one finding as it is: once an issue is closed and the list was read, every finished claim is drawn as done, whatever its verdict.

+

Try it

Open the issue and see where the four finished claims go; then say what a closed issue hides.

+

Open the interactive lesson →

+
The public board: which column a task's card is in
The public board: which column a task's card is in ↗
+

Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict.

+

specs/queen/dispatch.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done
+; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written
+; down in this family of repositories and only one of them moves work: the round of the trios supervisor
+; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it,
+; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with
+; the other cycles named as adapters and their gaps stated. Every decision below is a function the C
+; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py):
+;   ts      its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the
+;           public board's column, the single-flight round gate
+;   queend  its Swift policy binary, built at the pin and asked choose, review and retry
+;   pg      PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch
+;           writers on real rows, the criteria release, the CI take-back, the round's own SELECT
+;           (17.7 at this pin; the run at the old pin c25e1b02 used 16.15)
+;   lotus   gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which
+;           this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin
+;           c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host)
+;   live    one snapshot of the public endpoints of the deployed supervisor
+; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch
+; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read
+; from the source and named below: runner lanes (a claim in state queued that takes no worker slot,
+; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees)
+; and the public card's verdict and judgedHead.
+; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a
+; revision other than the pin. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDispatch;
+
+pub const KIND : str = "queen-dispatch";
+pub const ID : str = "queen/dispatch";
+pub const NAME : str = "The Queen's task lifecycle, from candidate to done";
+pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS";
+pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size";
+pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef";
+pub const CONSUMER_REPO : str = "gHashTag/trinity";
+pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
+pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530";
+pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149";
+; --- The decision: one canonical machine, the others are adapters ---------------------------------
+pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)";
+pub const ADAPTERS : [5]str = [
+    "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend",
+    "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design",
+    "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing",
+    "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task",
+    "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here"
+];
+pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state";
+pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"];
+; --- States of a dispatch row, and the events that move it ----------------------------------------
+pub const S_NONE : u32 = 0;
+pub const S_RUNNING : u32 = 1;
+pub const S_REFUSED : u32 = 2;
+pub const S_ENDED : u32 = 3;
+pub const S_WAIT : u32 = 4;
+pub const S_ACCEPT : u32 = 5;
+pub const S_SENDBACK : u32 = 6;
+pub const S_ESCALATE : u32 = 7;
+pub const S_EMPTY : u32 = 8;
+pub const S_STALE : u32 = 9;
+pub const S_REAPED : u32 = 10;
+pub const S_RELEASED : u32 = 11;
+pub const ILLEGAL : u32 = 99;
+pub const E_START : u32 = 0;
+pub const E_REFUSE : u32 = 1;
+pub const E_END : u32 = 2;
+pub const E_END_OTHER_TURN : u32 = 3;
+pub const E_REAP_STALL : u32 = 4;
+pub const E_REAP_BOOT : u32 = 5;
+pub const E_VERDICT_ACCEPT : u32 = 6;
+pub const E_VERDICT_SENDBACK : u32 = 7;
+pub const E_VERDICT_ESCALATE : u32 = 8;
+pub const E_VERDICT_WAIT : u32 = 9;
+pub const E_VERDICT_EMPTY : u32 = 10;
+pub const E_CRITERIA_CHANGED : u32 = 11;
+pub const E_CI_RED_UNDER_CEILING : u32 = 12;
+pub const E_CI_RED_AT_CEILING : u32 = 13;
+pub const E_FLOOR_PASSED : u32 = 14;
+pub const E_REDISPATCH : u32 = 15;
+pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)";
+pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation";
+pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it";
+; --- Claims: what a row costs its issue ------------------------------------------------------------
+pub const V_NONE : u32 = 0;
+pub const V_BLANK : u32 = 1;
+pub const V_WAIT : u32 = 2;
+pub const V_EMPTY : u32 = 3;
+pub const V_SENDBACK : u32 = 4;
+pub const V_ESCALATE : u32 = 5;
+pub const V_ACCEPT : u32 = 6;
+pub const V_FAILED : u32 = 7;
+pub const V_CANCELLED : u32 = 8;
+pub const V_STALE : u32 = 9;
+pub const V_OTHER : u32 = 10;
+pub const C_RUNNING : u32 = 0;
+pub const C_ACCEPTED : u32 = 1;
+pub const C_REJECTED : u32 = 2;
+pub const C_AWAITING_REVIEW : u32 = 3;
+pub const C_FAILED : u32 = 4;
+pub const EMPTY_FLOOR_MS : u64 = 1800000;
+pub const SENDBACK_FLOOR_MS : u64 = 3600000;
+pub const WAIT_FLOOR_MS : u64 = 21600000;
+pub const CEILING_RELEASE_MS : u64 = 3600000;
+pub const MAX_CEILING_RELEASES : u32 = 1;
+pub const SENDBACK_CEILING : u32 = 2;
+pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it";
+; --- Choosing ----------------------------------------------------------------------------------------
+pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates";
+pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies";
+pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing";
+pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only";
+pub const DEFAULT_WORKER_LIMIT : u32 = 4;
+pub const NONE_CHOSEN : u32 = 5;
+pub const COL_BACKLOG : u32 = 0;
+pub const COL_BLOCKED : u32 = 1;
+pub const COL_RUNNING : u32 = 2;
+pub const COL_REVIEW : u32 = 3;
+pub const COL_DONE : u32 = 4;
+pub const COL_DROPPED : u32 = 5;
+; --- Leases ------------------------------------------------------------------------------------------
+pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced";
+pub const LEASE_TTL_SECONDS : u32 = 180;
+pub const LEASE_HEARTBEAT_SECONDS : u32 = 60;
+pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane";
+pub const STALL_MINUTES : u32 = 120;
+; --- Review, retries, evidence, cancellation --------------------------------------------------------
+pub const R_ACCEPT : u32 = 0;
+pub const R_SENDBACK : u32 = 1;
+pub const R_ESCALATE : u32 = 2;
+pub const R_WAIT : u32 = 3;
+pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)";
+pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container";
+pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists";
+pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict";
+pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped";
+; --- Events, persistence, idempotency, projections --------------------------------------------------
+pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)";
+pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round";
+pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"];
+pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board";
+
+; --- End reasons for runs that produce no commits -----------------------------------------------------------
+; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted.
+; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days.
+pub const R_MODEL_ENDED_TURN : u32 = 0;      // Model ended its turn without editing any files
+pub const R_TIME_TOKEN_LIMIT : u32 = 1;     // Hit time or token limit during execution
+pub const R_CRASHED : u32 = 2;             // Container or tool crashed (with exit code)
+pub const R_BOUNDARY_CORRECT : u32 = 3;     // Boundary file was already correct, no changes needed
+pub const R_REFUSAL_START : u32 = 4;       // Refusal before the run started (invalid task, missing resources)
+pub const R_PUSH_FAILED : u32 = 5;          // Push to remote failed after successful run
+pub const R_UNKNOWN : u32 = 6;             // Cause could not be determined by the runner
+
+pub const END_REASON_COUNT : u32 = 7;
+pub const END_REASON_NAMES : [7]str = [
+    "model_ended_turn",
+    "time_token_limit", 
+    "crashed",
+    "boundary_correct",
+    "refusal_start",
+    "push_failed",
+    "unknown"
+];
+
+; --- Public board extension: show end reason on empty cards -----------------------------------------------
+; The public board's "empty" cards now include the end reason when available, enabling breakdown of
+; the 56.0% empty-branch share by reason from public endpoints alone.
+pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available";
+pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints";
+; --- Agents and workers ---------------------------------------------------------------------------------
+pub const AGENT_LETTERS : u32 = 27;
+pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27";
+; --- The specs this one sits beside ----------------------------------------------------------------------
+pub const NEIGHBOURS : [6]str = [
+    "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master",
+    "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort",
+    "specs/queen/brain_summaries.t27: does not compile in C at master",
+    "specs/brain/brain.t27: no function; does not compile in C at master",
+    "specs/brain/bus.t27: compiles, one test of a version constant",
+    "specs/brain/cognitive_loop.t27: compiles, one test of a version constant"
+];
+; --- Findings: measured, the tag names the evidence ------------------------------------------------------
+pub const FINDINGS_COUNT : u32 = 15;
+pub const FINDINGS : [15]str = [
+    "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]",
+    "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]",
+    "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]",
+    "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]",
+    "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]",
+    "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]",
+    "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]",
+    "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]",
+    "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]",
+    "f59 nothing cancels a running bee [source]",
+    "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]",
+    "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]",
+    "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]",
+    "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]",
+    "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]"
+];
+pub const ENABLED : bool = true;
+
+; --- Rules ------------------------------------------------------------------------------------------------------
+
+pub fn next_state(state: u32, event: u32) -> u32 {
+    if (state == S_NONE) {
+        if (event == E_START) { return S_RUNNING; }
+        if (event == E_REFUSE) { return S_REFUSED; }
+        return ILLEGAL;
+    }
+    if (state == S_RUNNING) {
+        if (event == E_END) { return S_ENDED; }
+        if (event == E_REAP_STALL) { return S_REAPED; }
+        if (event == E_REAP_BOOT) { return S_REAPED; }
+        return ILLEGAL;
+    }
+    if (state == S_ENDED or state == S_WAIT) {
+        if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; }
+        if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; }
+        if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; }
+        if (event == E_VERDICT_WAIT) { return S_WAIT; }
+        if (event == E_VERDICT_EMPTY) { return S_EMPTY; }
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ACCEPT) {
+        if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; }
+        if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; }
+        return ILLEGAL;
+    }
+    if (state == S_SENDBACK or state == S_EMPTY) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ESCALATE) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        return ILLEGAL;
+    }
+    if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) {
+        if (event == E_REDISPATCH) { return S_RUNNING; }
+        return ILLEGAL;
+    }
+    return ILLEGAL;
+}
+
+pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 {
+    if (finished == false) { return C_RUNNING; }
+    if (verdict == V_ACCEPT) { return C_ACCEPTED; }
+    if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; }
+    if (verdict == V_EMPTY) {
+        if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_STALE) { return C_FAILED; }
+    if (verdict == V_SENDBACK) {
+        if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+        if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; }
+        if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) {
+        if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+    }
+    return C_AWAITING_REVIEW;
+}
+
+; What the round and the board actually pass for `releases`: neither selects the column.
+pub fn releases_read_by_round(stored: u32) -> u32 {
+    return 0;
+}
+
+pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 {
+    if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; }
+    if (claim == C_RUNNING) { return COL_RUNNING; }
+    if (claim == C_ACCEPTED) { return COL_DONE; }
+    if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; }
+    return COL_DROPPED;
+}
+
+pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 {
+    if (e0) { return 0; }
+    if (e1) { return 1; }
+    if (e2) { return 2; }
+    if (e3) { return 3; }
+    if (e4) { return 4; }
+    return NONE_CHOSEN;
+}
+
+pub fn can_start(running: u32, limit: u32) -> bool {
+    return running < limit;
+}
+
+pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 {
+    if (total == 0) { return R_ESCALATE; }
+    if (judged < total) { return R_WAIT; }
+    if (met == total) {
+        if (committed_known and committed > 0) { return R_ACCEPT; }
+        return R_ESCALATE;
+    }
+    if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; }
+    return R_SENDBACK;
+}
+
+pub fn retry_allowed(real_attempts: u32) -> bool {
+    return real_attempts < SENDBACK_CEILING;
+}
+
+pub fn counts_against_issue(interrupted: bool) -> bool {
+    return interrupted == false;
+}
+
+pub fn lease_taken(expired: bool, same_holder: bool) -> bool {
+    return expired or same_holder;
+}
+
+pub fn fence_after(fence: u64) -> u64 {
+    return fence + 1;
+}
+
+pub fn owners_per_term() -> u32 {
+    return 1;
+}
+
+pub fn stall_reapable(age_minutes: u32) -> bool {
+    return age_minutes > STALL_MINUTES;
+}
+
+pub fn followup_rounds(requests_during_round: u32) -> u32 {
+    if (requests_during_round > 0) { return 1; }
+    return 0;
+}
+
+pub fn accept_requires_pull_request() -> bool {
+    return false;
+}
+
+pub fn writer_enforces_table() -> bool {
+    return false;
+}
+
+invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; }
+invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; }
+invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; }
+invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; }
+invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); }
+invariant all_end_reasons_valid { 
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN);
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT);
+    assert is_valid_end_reason(R_CRASHED);
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT);
+    assert is_valid_end_reason(R_REFUSAL_START);
+    assert is_valid_end_reason(R_PUSH_FAILED);
+    assert is_valid_end_reason(R_UNKNOWN);
+}
+
+; --- End reason handling functions --------------------------------------------------------------------------
+
+pub fn is_valid_end_reason(reason: u32) -> bool {
+    return reason < END_REASON_COUNT;
+}
+
+pub fn end_reason_to_name(reason: u32) -> str {
+    if (is_valid_end_reason(reason)) {
+        return END_REASON_NAMES[reason];
+    }
+    return "invalid_end_reason";
+}
+
+pub fn is_no_diff_end_reason(reason: u32) -> bool {
+    return reason == R_MODEL_ENDED_TURN 
+        or reason == R_TIME_TOKEN_LIMIT 
+        or reason == R_BOUNDARY_CORRECT 
+        or reason == R_REFUSAL_START 
+        or reason == R_PUSH_FAILED 
+        or reason == R_UNKNOWN;
+}
+
+pub fn is_crash_end_reason(reason: u32) -> bool {
+    return reason == R_CRASHED;
+}
+
+test the_table_moves_only_along_its_guards {
+    assert next_state(S_NONE, E_START) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END) == S_ENDED;
+    assert next_state(S_ENDED, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE;
+    assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL;
+    assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL;
+    // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886).
+    assert next_state(S_NONE, E_REFUSE) == S_REFUSED;
+    assert next_state(S_NONE, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED;
+    assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED;
+    assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK;
+    assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE;
+    assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT;
+    assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY;
+    assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK;
+    assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_START) == ILLEGAL;
+    assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING;
+}
+
+test a_claim_follows_the_verdict_and_three_clocks {
+    assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING;
+    assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED;
+    assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED;
+    assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW;
+    // #9886: each verdict that frees the issue, the send-back floor at its edge with a release
+    // already spent, and the wait floor's two edges.
+    assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED;
+    assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW;
+    assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW;
+}
+
+test a_closed_issue_is_drawn_done_whatever_its_verdict {
+    assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE;
+    assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW;
+    assert column_of(C_RUNNING, false, true) == COL_RUNNING;
+    assert column_of(C_FAILED, true, true) == COL_DROPPED;
+    assert column_of(C_ACCEPTED, true, true) == COL_DONE;
+}
+
+test the_first_eligible_wins_in_any_order {
+    assert chosen_of(false, false, true, true, false) == 2;
+    assert chosen_of(false, true, false, false, true) == 1;
+    assert chosen_of(false, false, false, false, false) == NONE_CHOSEN;
+    assert chosen_of(true, true, false, false, false) == 0;
+    assert chosen_of(false, false, false, true, true) == 3;
+    assert chosen_of(false, false, false, false, true) == 4;
+    assert can_start(3, DEFAULT_WORKER_LIMIT) == true;
+    assert can_start(4, DEFAULT_WORKER_LIMIT) == false;
+}
+
+test review_escalates_without_work_and_after_two_send_backs {
+    assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE;
+    assert review_of(2, 1, 1, true, 1, 0) == R_WAIT;
+    assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT;
+    assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK;
+    assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE;
+    assert retry_allowed(1) == true;
+    assert retry_allowed(2) == false;
+    assert counts_against_issue(true) == false;
+    assert counts_against_issue(false) == true;
+}
+
+test one_lease_one_owner_and_age_alone_reaps {
+    assert lease_taken(false, false) == false;
+    assert lease_taken(true, false) == true;
+    assert lease_taken(false, true) == true;
+    assert fence_after(3) == 4;
+    assert owners_per_term() == 1;
+    assert stall_reapable(121) == true;
+    assert stall_reapable(119) == false;
+    // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself.
+    assert stall_reapable(120) == false;
+    assert followup_rounds(5) == 1;
+    assert followup_rounds(0) == 0;
+    assert accept_requires_pull_request() == false;
+    assert writer_enforces_table() == false;
+}
+
+; --- End reason tests --------------------------------------------------------------------------------------
+
+test end_reason_constants_are_correct {
+    assert R_MODEL_ENDED_TURN == 0;
+    assert R_TIME_TOKEN_LIMIT == 1;
+    assert R_CRASHED == 2;
+    assert R_BOUNDARY_CORRECT == 3;
+    assert R_REFUSAL_START == 4;
+    assert R_PUSH_FAILED == 5;
+    assert R_UNKNOWN == 6;
+    assert END_REASON_COUNT == 7;
+    // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two
+    // call the functions that read the constants: the last reason is named, one past it is not.
+    assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown";
+    assert is_valid_end_reason(END_REASON_COUNT) == false;
+}
+
+test end_reason_names_array_matches_constants {
+    assert END_REASON_NAMES[0] == "model_ended_turn";
+    assert END_REASON_NAMES[1] == "time_token_limit";
+    assert END_REASON_NAMES[2] == "crashed";
+    assert END_REASON_NAMES[3] == "boundary_correct";
+    assert END_REASON_NAMES[4] == "refusal_start";
+    assert END_REASON_NAMES[5] == "push_failed";
+    assert END_REASON_NAMES[6] == "unknown";
+}
+
+test end_reason_validation_works {
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_valid_end_reason(R_CRASHED) == true;
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_valid_end_reason(R_REFUSAL_START) == true;
+    assert is_valid_end_reason(R_PUSH_FAILED) == true;
+    assert is_valid_end_reason(R_UNKNOWN) == true;
+    assert is_valid_end_reason(7) == false;  // One past the last valid reason
+    assert is_valid_end_reason(99) == false; // Way past the last valid reason
+}
+
+test end_reason_to_name_conversion {
+    assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn";
+    assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit";
+    assert end_reason_to_name(R_CRASHED) == "crashed";
+    assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct";
+    assert end_reason_to_name(R_REFUSAL_START) == "refusal_start";
+    assert end_reason_to_name(R_PUSH_FAILED) == "push_failed";
+    assert end_reason_to_name(R_UNKNOWN) == "unknown";
+    assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string
+}
+
+test no_diff_end_reason_classification {
+    assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_no_diff_end_reason(R_REFUSAL_START) == true;
+    assert is_no_diff_end_reason(R_PUSH_FAILED) == true;
+    assert is_no_diff_end_reason(R_UNKNOWN) == true;
+    assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category
+}
+
+test crash_end_reason_classification {
+    assert is_crash_end_reason(R_CRASHED) == true;
+    assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false;
+    assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false;
+    assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false;
+    assert is_crash_end_reason(R_REFUSAL_START) == false;
+    assert is_crash_end_reason(R_PUSH_FAILED) == false;
+    assert is_crash_end_reason(R_UNKNOWN) == false;
+}
+
+test end_reason_enables_breakdown_of_empty_branches {
+    // This test validates that the end reason vocabulary enables breaking down
+    // the 56.0% empty-branch share by reason from public endpoints alone
+    
+    // Simulate counts for empty branches by reason
+    var model_ended_count : u32 = 100;
+    var token_limit_count : u32 = 80;
+    var boundary_correct_count : u32 = 60;
+    var refusal_start_count : u32 = 40;
+    var push_failed_count : u32 = 30;
+    var unknown_count : u32 = 7;  // Should be < 10% of total
+    var crash_count : u32 = 0;
+    
+    var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                           refusal_start_count + push_failed_count + unknown_count + crash_count;
+    
+    // Verify unknown is less than 10% of total (success criterion from issue)
+    assert unknown_count < total_empty / 10;
+    
+    // Verify we can calculate breakdown percentages
+    var model_percentage : u32 = (model_ended_count * 100) / total_empty;
+    var token_percentage : u32 = (token_limit_count * 100) / total_empty;
+    var unknown_percentage : u32 = (unknown_count * 100) / total_empty;
+    
+    assert model_percentage > 0;
+    assert token_percentage > 0;
+    assert unknown_percentage < 10;  // Unknown should be < 10%
+    
+    // Verify no-diff vs crash categorization works
+    var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                              refusal_start_count + push_failed_count + unknown_count;
+    var crash_total : u32 = crash_count;
+    
+    assert no_diff_total == total_empty - crash_total;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-queens-move/card.png b/apps/website/public/learn/the-queens-move/card.png new file mode 100644 index 0000000000..655cf30c1b Binary files /dev/null and b/apps/website/public/learn/the-queens-move/card.png differ diff --git a/apps/website/public/learn/the-queens-move/index.html b/apps/website/public/learn/the-queens-move/index.html new file mode 100644 index 0000000000..5f9f35dc9f --- /dev/null +++ b/apps/website/public/learn/the-queens-move/index.html @@ -0,0 +1,349 @@ + + + + + + +The Queen's move: the Queen and the hive course, lesson 14 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The Queen's move

+

You will learn

What the Queen does for each jam, and which jams she waits on.

+

Each case has one action at the same position: no reviewer dispatches a reviewer, refused not fixed dispatches the coder, refused then pushed dispatches a re-review, and labeled not merged finishes the merge. jam_action maps any value above 9 to measure again, so an unknown case is never nothing to do. jam_is_queens_turn is false for 3 cases, none, draft or fresh, and stacked on an open PR: she waits on those.

+

Try it

Find the case whose action is wait for the base; then drag to 200 and read the action.

+

Open the interactive lesson →

+
The Queen's move: what each jam asks of her
The Queen's move: what each jam asks of her ↗
+

Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting.

+

specs/automation/review-jam.t27

// Review jam: which case a stuck pull request is in, and the Queen's action.
+// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700).
+// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead
+// of hand-copying them.
+
+module review_jam;
+
+// Jam case constants (u8, value = position, starting at 0)
+pub const JAM_NONE: u8 = 0;
+pub const JAM_NO_REVIEWER: u8 = 1;
+pub const JAM_STACKED_ON_OPEN: u8 = 2;
+pub const JAM_REFUSED_NOT_FIXED: u8 = 3;
+pub const JAM_REFUSED_THEN_PUSHED: u8 = 4;
+pub const JAM_LABELED_NOT_MERGED: u8 = 5;
+pub const JAM_CONFLICTING: u8 = 6;
+pub const JAM_NOISE_RED: u8 = 7;
+pub const JAM_DRAFT_OR_FRESH: u8 = 8;
+pub const JAM_UNMEASURED: u8 = 9;
+
+// Queen action constants (u8, value = position, starting at 0)
+pub const ACT_NOTHING: u8 = 0;
+pub const ACT_DISPATCH_REVIEWER: u8 = 1;
+pub const ACT_WAIT_FOR_BASE: u8 = 2;
+pub const ACT_DISPATCH_CODER: u8 = 3;
+pub const ACT_DISPATCH_RE_REVIEW: u8 = 4;
+pub const ACT_FINISH_MERGE: u8 = 5;
+pub const ACT_REBASE_BY_CODER: u8 = 6;
+pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7;
+pub const ACT_WAIT: u8 = 8;
+pub const ACT_MEASURE_AGAIN: u8 = 9;
+
+// The jam case of a pull request. The FIRST matching rule wins:
+//  1. not measured                      -> UNMEASURED
+//  2. draft or fresh                    -> DRAFT_OR_FRESH
+//  3. stacked on an open PR             -> STACKED_ON_OPEN
+//  4. conflicting                       -> CONFLICTING
+//  5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
+//  6. refused and pushed after          -> REFUSED_THEN_PUSHED
+//  7. refused                           -> REFUSED_NOT_FIXED
+//  8. only noise checks red             -> NOISE_RED
+//  9. no reviewer                       -> NO_REVIEWER
+// 10. otherwise                         -> NONE
+pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 {
+    if !measured {
+        return JAM_UNMEASURED;
+    }
+    if draft_or_fresh {
+        return JAM_DRAFT_OR_FRESH;
+    }
+    if stacked_on_open {
+        return JAM_STACKED_ON_OPEN;
+    }
+    if conflicting {
+        return JAM_CONFLICTING;
+    }
+    if labeled_reviewed {
+        return JAM_LABELED_NOT_MERGED;
+    }
+    if refused && pushed_after_refusal {
+        return JAM_REFUSED_THEN_PUSHED;
+    }
+    if refused {
+        return JAM_REFUSED_NOT_FIXED;
+    }
+    if only_noise_red {
+        return JAM_NOISE_RED;
+    }
+    if !has_reviewer {
+        return JAM_NO_REVIEWER;
+    }
+    JAM_NONE
+}
+
+// The Queen's action for a case: the action with the SAME position.
+// Any value above 9 is an unknown case, and an unknown case is never
+// "nothing to do": it maps to ACT_MEASURE_AGAIN.
+pub fn jam_action(case: u8) -> u8 {
+    if case > 9 {
+        return ACT_MEASURE_AGAIN;
+    }
+    case
+}
+
+// Whether the case is the Queen's turn to act: true for every case except
+// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN.
+pub fn jam_is_queens_turn(case: u8) -> bool {
+    case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN
+}
+
+// ---- jam_case: one test per case ----
+
+test "unmeasured beats every other flag" {
+    assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
+}
+
+test "unmeasured with every other flag clear" {
+    assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED;
+}
+
+test "draft or fresh" {
+    assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked on open PR" {
+    assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting" {
+    assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled not merged" {
+    assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed" {
+    assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed" {
+    assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red" {
+    assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED;
+}
+
+test "no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER;
+}
+
+test "none" {
+    assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE;
+}
+
+// ---- jam_case: first-match order, each rule against the next one ----
+
+test "draft beats stacked" {
+    assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked and unreviewed is stacked, not no reviewer" {
+    assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "stacked beats conflicting" {
+    assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting beats labeled" {
+    assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled beats refused then pushed" {
+    assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red beats no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
+}
+
+test "pushed without a refusal is not refused then pushed" {
+    assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE;
+}
+
+// ---- jam_action: same position, unknown -> measure again ----
+
+test "action none to nothing" {
+    assert jam_action(JAM_NONE) == ACT_NOTHING;
+}
+
+test "action no reviewer to dispatch reviewer" {
+    assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
+}
+
+test "action stacked to wait for base" {
+    assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE;
+}
+
+test "action refused not fixed to dispatch coder" {
+    assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER;
+}
+
+test "action refused then pushed to dispatch re-review" {
+    assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW;
+}
+
+test "action labeled not merged to finish merge" {
+    assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE;
+}
+
+test "action conflicting to rebase by coder" {
+    assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER;
+}
+
+test "action noise red to name noise and review" {
+    assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW;
+}
+
+test "action draft or fresh to wait" {
+    assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT;
+}
+
+test "action unmeasured to measure again" {
+    assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN;
+}
+
+test "action first unknown case measures again" {
+    assert jam_action(10) == ACT_MEASURE_AGAIN;
+}
+
+test "action unknown case 200 measures again" {
+    assert jam_action(200) == ACT_MEASURE_AGAIN;
+}
+
+// ---- jam_is_queens_turn ----
+
+test "queens turn for no reviewer" {
+    assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
+}
+
+test "queens turn for refused not fixed" {
+    assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
+}
+
+test "queens turn for refused then pushed" {
+    assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
+}
+
+test "queens turn for labeled not merged" {
+    assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
+}
+
+test "queens turn for conflicting" {
+    assert jam_is_queens_turn(JAM_CONFLICTING) == true;
+}
+
+test "queens turn for noise red" {
+    assert jam_is_queens_turn(JAM_NOISE_RED) == true;
+}
+
+test "queens turn for unmeasured" {
+    assert jam_is_queens_turn(JAM_UNMEASURED) == true;
+}
+
+test "not queens turn for none" {
+    assert jam_is_queens_turn(JAM_NONE) == false;
+}
+
+test "not queens turn for draft or fresh" {
+    assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
+}
+
+test "not queens turn for stacked on open" {
+    assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
+}
+
+// ---- the two scenarios of #9032, end to end ----
+
+test "scenario bee-reviewed but still open finishes the merge" {
+    assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE;
+}
+
+test "scenario failed read is never reported as free" {
+    assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-queue/card.png b/apps/website/public/learn/the-queue/card.png new file mode 100644 index 0000000000..7e8de34cce Binary files /dev/null and b/apps/website/public/learn/the-queue/card.png differ diff --git a/apps/website/public/learn/the-queue/index.html b/apps/website/public/learn/the-queue/index.html new file mode 100644 index 0000000000..ae4399f359 --- /dev/null +++ b/apps/website/public/learn/the-queue/index.html @@ -0,0 +1,238 @@ + + + + + + +The queue: the Queen and the hive course, lesson 5 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The queue

+

You will learn

How the cap, aging and blockers order a queue of open issues.

+

Labels inflate: if every issue is critical, none is. capped_level lets at most CRITICAL_CAP, 3, issues of a listing run as critical, taken in listing order, and the 4th runs as high. An issue with an open blocker is skipped, not waited on, so it cannot hold up the queue. outranks compares the effective level, then the real level, then the place in the listing, so an aged issue never beats a real one of its level. why names the reason for each place.

+

Try it

Make a fifth issue critical and see where it lands; then label the unlabelled issue low.

+

Open the interactive lesson →

+
The queue: which issue the Queen takes first
The queue: which issue the Queen takes first ↗
+

Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there.

+

specs/queen/priority.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366)
+; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no
+; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it.
+; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js`
+; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes.
+; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank,
+; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort):
+;   - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first);
+;   - an issue with an open blocker is skipped, not waited on (head-of-line blocking);
+;   - a cap on critical stops priority inflation; aging stops starvation of labelled low work;
+;   - an issue nobody labelled keeps today's place, so a repository with no labels sees no change.
+; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority
+; field (read it first once the org enables it; its four values map onto the same levels).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenPriority;
+
+/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages.
+pub const LEVEL_CRITICAL : u8 = 0;
+pub const LEVEL_HIGH : u8 = 1;
+pub const LEVEL_NORMAL : u8 = 2;
+pub const LEVEL_LOW : u8 = 3;
+
+/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing.
+/// When an issue carries labels of two levels, the more urgent one counts.
+pub const CRITICAL_LABELS : str = "P0,priority/critical,critical";
+pub const HIGH_LABELS : str = "P1,priority/high,high-priority";
+pub const NORMAL_LABELS : str = "P2,priority/medium";
+pub const LOW_LABELS : str = "P3,P4,priority/low";
+
+/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels,
+/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low"
+/// the fastest way to the front of a long queue.
+pub const AGING_DAYS : u32 = 14;
+pub const MAX_AGING_STEPS : u32 = 1;
+
+/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH.
+pub const CRITICAL_CAP : u32 = 3;
+
+/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs.
+pub const WHY_LISTING : u8 = 0;
+pub const WHY_LABEL : u8 = 1;
+pub const WHY_AGED : u8 = 2;
+pub const WHY_CAPPED : u8 = 3;
+pub const WHY_BLOCKED : u8 = 4;
+
+/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one.
+pub fn capped_level(base: u8, criticals_before: u32) -> u8 {
+    if (base == LEVEL_CRITICAL) {
+        if (criticals_before >= CRITICAL_CAP) {
+            return LEVEL_HIGH;
+        }
+    }
+    return base;
+}
+
+/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS)
+/// when it is labelled.
+pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 {
+    if (labelled == false) {
+        return level;
+    }
+    if (level <= LEVEL_HIGH) {
+        return level;
+    }
+    var steps : u32 = age_days / AGING_DAYS;
+    if (steps > MAX_AGING_STEPS) {
+        steps = MAX_AGING_STEPS;
+    }
+    var gap : u32 = (level as u32) - (LEVEL_HIGH as u32);
+    if (steps >= gap) {
+        return LEVEL_HIGH;
+    }
+    return ((level as u32) - steps) as u8;
+}
+
+/// An issue with an open blocker is not a candidate; the Queen moves past it.
+pub fn eligible(open_blockers: u32) -> bool {
+    return open_blockers == 0;
+}
+
+/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged
+/// issue never beats a real one of the same level), then earlier in GitHub's listing.
+pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool {
+    if (eff_a < eff_b) { return true; }
+    if (eff_a > eff_b) { return false; }
+    if (level_a < level_b) { return true; }
+    if (level_a > level_b) { return false; }
+    return index_a < index_b;
+}
+
+/// The reason printed for one issue.
+pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 {
+    if (open_blockers > 0) { return WHY_BLOCKED; }
+    if (level != base) { return WHY_CAPPED; }
+    if (eff != level) { return WHY_AGED; }
+    if (labelled) { return WHY_LABEL; }
+    return WHY_LISTING;
+}
+
+test no_label_keeps_the_listing_order {
+    assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1);
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false;
+    // #7077: a strict order, so a sort can use it: no issue outranks itself
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false;
+}
+
+test critical_runs_before_a_newer_unlabelled_issue {
+    assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0);
+}
+
+test unlabelled_never_beats_urgent {
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false;
+}
+
+test labelled_work_ages_one_level_and_never_to_critical {
+    assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW;
+    assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH;
+    assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH;
+}
+
+test an_aged_issue_loses_the_tie_to_a_real_one {
+    var aged : u8 = effective_level(LEVEL_LOW, true, 28);
+    assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0);
+    // #7077: of two LOW labels the aged one goes first, whatever the listing says
+    assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0);
+    assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false;
+}
+
+test the_fourth_critical_runs_as_high {
+    assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH;
+    assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW;
+}
+
+test a_blocked_issue_is_skipped {
+    assert eligible(0);
+    assert eligible(1) == false;
+    assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED;
+}
+
+test every_reason_is_reachable {
+    assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING;
+    assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL;
+    assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED;
+    assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED;
+}
+
+invariant levels_are_ordered {
+    assert LEVEL_CRITICAL < LEVEL_HIGH;
+    assert LEVEL_HIGH < LEVEL_NORMAL;
+    assert LEVEL_NORMAL < LEVEL_LOW;
+    assert CRITICAL_CAP > 0;
+    assert AGING_DAYS > 0;
+    assert MAX_AGING_STEPS == 1;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-review-jam/card.png b/apps/website/public/learn/the-review-jam/card.png new file mode 100644 index 0000000000..a436d2a88e Binary files /dev/null and b/apps/website/public/learn/the-review-jam/card.png differ diff --git a/apps/website/public/learn/the-review-jam/index.html b/apps/website/public/learn/the-review-jam/index.html new file mode 100644 index 0000000000..0ed55cb082 --- /dev/null +++ b/apps/website/public/learn/the-review-jam/index.html @@ -0,0 +1,349 @@ + + + + + + +The review jam: the Queen and the hive course, lesson 13 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The review jam

+

You will learn

Which of ten cases a stuck pull request is in, and why the earliest matching rule wins.

+

A pull request can stall for many reasons at once: refused, pushed again, and red on a noise check. review-jam.t27 gives the Queen one answer. jam_case checks 9 facts in a fixed order and the earliest rule that matches wins, so a PR the Queen could not measure is never reported as free, and one that was refused and then pushed is not taken for noise. There are 10 cases, from none to unmeasured.

+

Try it

Clear facts one by one from the start state and name each case you pass; then find a PR that is in no jam.

+

Open the interactive lesson →

+
The review jam: which case a stuck pull request is in
The review jam: which case a stuck pull request is in ↗
+

Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule.

+

specs/automation/review-jam.t27

// Review jam: which case a stuck pull request is in, and the Queen's action.
+// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700).
+// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead
+// of hand-copying them.
+
+module review_jam;
+
+// Jam case constants (u8, value = position, starting at 0)
+pub const JAM_NONE: u8 = 0;
+pub const JAM_NO_REVIEWER: u8 = 1;
+pub const JAM_STACKED_ON_OPEN: u8 = 2;
+pub const JAM_REFUSED_NOT_FIXED: u8 = 3;
+pub const JAM_REFUSED_THEN_PUSHED: u8 = 4;
+pub const JAM_LABELED_NOT_MERGED: u8 = 5;
+pub const JAM_CONFLICTING: u8 = 6;
+pub const JAM_NOISE_RED: u8 = 7;
+pub const JAM_DRAFT_OR_FRESH: u8 = 8;
+pub const JAM_UNMEASURED: u8 = 9;
+
+// Queen action constants (u8, value = position, starting at 0)
+pub const ACT_NOTHING: u8 = 0;
+pub const ACT_DISPATCH_REVIEWER: u8 = 1;
+pub const ACT_WAIT_FOR_BASE: u8 = 2;
+pub const ACT_DISPATCH_CODER: u8 = 3;
+pub const ACT_DISPATCH_RE_REVIEW: u8 = 4;
+pub const ACT_FINISH_MERGE: u8 = 5;
+pub const ACT_REBASE_BY_CODER: u8 = 6;
+pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7;
+pub const ACT_WAIT: u8 = 8;
+pub const ACT_MEASURE_AGAIN: u8 = 9;
+
+// The jam case of a pull request. The FIRST matching rule wins:
+//  1. not measured                      -> UNMEASURED
+//  2. draft or fresh                    -> DRAFT_OR_FRESH
+//  3. stacked on an open PR             -> STACKED_ON_OPEN
+//  4. conflicting                       -> CONFLICTING
+//  5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
+//  6. refused and pushed after          -> REFUSED_THEN_PUSHED
+//  7. refused                           -> REFUSED_NOT_FIXED
+//  8. only noise checks red             -> NOISE_RED
+//  9. no reviewer                       -> NO_REVIEWER
+// 10. otherwise                         -> NONE
+pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 {
+    if !measured {
+        return JAM_UNMEASURED;
+    }
+    if draft_or_fresh {
+        return JAM_DRAFT_OR_FRESH;
+    }
+    if stacked_on_open {
+        return JAM_STACKED_ON_OPEN;
+    }
+    if conflicting {
+        return JAM_CONFLICTING;
+    }
+    if labeled_reviewed {
+        return JAM_LABELED_NOT_MERGED;
+    }
+    if refused && pushed_after_refusal {
+        return JAM_REFUSED_THEN_PUSHED;
+    }
+    if refused {
+        return JAM_REFUSED_NOT_FIXED;
+    }
+    if only_noise_red {
+        return JAM_NOISE_RED;
+    }
+    if !has_reviewer {
+        return JAM_NO_REVIEWER;
+    }
+    JAM_NONE
+}
+
+// The Queen's action for a case: the action with the SAME position.
+// Any value above 9 is an unknown case, and an unknown case is never
+// "nothing to do": it maps to ACT_MEASURE_AGAIN.
+pub fn jam_action(case: u8) -> u8 {
+    if case > 9 {
+        return ACT_MEASURE_AGAIN;
+    }
+    case
+}
+
+// Whether the case is the Queen's turn to act: true for every case except
+// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN.
+pub fn jam_is_queens_turn(case: u8) -> bool {
+    case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN
+}
+
+// ---- jam_case: one test per case ----
+
+test "unmeasured beats every other flag" {
+    assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
+}
+
+test "unmeasured with every other flag clear" {
+    assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED;
+}
+
+test "draft or fresh" {
+    assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked on open PR" {
+    assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting" {
+    assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled not merged" {
+    assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed" {
+    assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed" {
+    assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red" {
+    assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED;
+}
+
+test "no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER;
+}
+
+test "none" {
+    assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE;
+}
+
+// ---- jam_case: first-match order, each rule against the next one ----
+
+test "draft beats stacked" {
+    assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked and unreviewed is stacked, not no reviewer" {
+    assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "stacked beats conflicting" {
+    assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting beats labeled" {
+    assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled beats refused then pushed" {
+    assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red beats no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
+}
+
+test "pushed without a refusal is not refused then pushed" {
+    assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE;
+}
+
+// ---- jam_action: same position, unknown -> measure again ----
+
+test "action none to nothing" {
+    assert jam_action(JAM_NONE) == ACT_NOTHING;
+}
+
+test "action no reviewer to dispatch reviewer" {
+    assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
+}
+
+test "action stacked to wait for base" {
+    assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE;
+}
+
+test "action refused not fixed to dispatch coder" {
+    assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER;
+}
+
+test "action refused then pushed to dispatch re-review" {
+    assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW;
+}
+
+test "action labeled not merged to finish merge" {
+    assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE;
+}
+
+test "action conflicting to rebase by coder" {
+    assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER;
+}
+
+test "action noise red to name noise and review" {
+    assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW;
+}
+
+test "action draft or fresh to wait" {
+    assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT;
+}
+
+test "action unmeasured to measure again" {
+    assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN;
+}
+
+test "action first unknown case measures again" {
+    assert jam_action(10) == ACT_MEASURE_AGAIN;
+}
+
+test "action unknown case 200 measures again" {
+    assert jam_action(200) == ACT_MEASURE_AGAIN;
+}
+
+// ---- jam_is_queens_turn ----
+
+test "queens turn for no reviewer" {
+    assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
+}
+
+test "queens turn for refused not fixed" {
+    assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
+}
+
+test "queens turn for refused then pushed" {
+    assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
+}
+
+test "queens turn for labeled not merged" {
+    assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
+}
+
+test "queens turn for conflicting" {
+    assert jam_is_queens_turn(JAM_CONFLICTING) == true;
+}
+
+test "queens turn for noise red" {
+    assert jam_is_queens_turn(JAM_NOISE_RED) == true;
+}
+
+test "queens turn for unmeasured" {
+    assert jam_is_queens_turn(JAM_UNMEASURED) == true;
+}
+
+test "not queens turn for none" {
+    assert jam_is_queens_turn(JAM_NONE) == false;
+}
+
+test "not queens turn for draft or fresh" {
+    assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
+}
+
+test "not queens turn for stacked on open" {
+    assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
+}
+
+// ---- the two scenarios of #9032, end to end ----
+
+test "scenario bee-reviewed but still open finishes the merge" {
+    assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE;
+}
+
+test "scenario failed read is never reported as free" {
+    assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/the-ship-flies-home/card.png b/apps/website/public/learn/the-ship-flies-home/card.png new file mode 100644 index 0000000000..e09cc8cc50 Binary files /dev/null and b/apps/website/public/learn/the-ship-flies-home/card.png differ diff --git a/apps/website/public/learn/the-ship-flies-home/index.html b/apps/website/public/learn/the-ship-flies-home/index.html new file mode 100644 index 0000000000..5f922d11d6 --- /dev/null +++ b/apps/website/public/learn/the-ship-flies-home/index.html @@ -0,0 +1,259 @@ + + + + + + +The ship flies home: the Queen and the hive course, lesson 8 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

The ship flies home

+

You will learn

Where the game draws a bee for each thing it is doing, and how a finished task ends on screen.

+

In the game at app.t27.ai/game each bee is a ship over the issue it works on. ship_mode in hive.t27 picks the place: parked at the entrance before a runner claims it, orbiting its cell while it is heard from, hovering when it has gone quiet, and waiting at its repository's portal until its cell is placed. When the bee leaves the feed it flies home, and ship_alpha fades it over DEPART_MS, 2400 ms: at 1200 ms it is at 500 of 1000.

+

Try it

Mark the bee as not heard from and see where it moves; then drag the time past 2400 ms.

+

Open the interactive lesson →

+
The game: where a bee flies and how it leaves
The game: where a bee flies and how it leaves ↗
+

Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out.

+

specs/queen/hive.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to
+; a cell, how a ship leaves, and how often the map may grow
+; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game
+; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4.
+; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day):
+; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen.
+; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far
+;   from the repository it works for.
+; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole
+;   scene: one rebuild for most bees that started.
+; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same
+;   as no news.
+; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two
+; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenHive;
+
+pub const HIVE_KINDS : u8 = 10;
+
+; --- 1. Where a ship flies ---------------------------------------------------------------------------
+
+/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this
+/// map can show (a repository with no region, or a task that is not an issue).
+pub const CELL_PLACED : u8 = 0;
+pub const CELL_PENDING : u8 = 1;
+pub const CELL_NONE : u8 = 2;
+
+/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim,
+/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is
+/// placed. HOME: flying back to the entrance, its task over.
+pub const SM_PARK : u8 = 0;
+pub const SM_ORBIT : u8 = 1;
+pub const SM_HOVER : u8 = 2;
+pub const SM_PORTAL : u8 = 3;
+pub const SM_HOME : u8 = 4;
+
+/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no
+/// longer in the feed.
+pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 {
+    if (departing) { return SM_HOME; }
+    if (queued) { return SM_PARK; }
+    if (cell == CELL_PENDING) { return SM_PORTAL; }
+    if (cell != CELL_PLACED) { return SM_PARK; }
+    if (working) { return SM_ORBIT; }
+    return SM_HOVER;
+}
+
+; --- 2. How a ship leaves ----------------------------------------------------------------------------
+
+/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing.
+pub const DEPART_MS : u32 = 2400;
+
+pub fn ship_kept(ms_since_left: u32) -> bool {
+    return ms_since_left < DEPART_MS;
+}
+
+/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing.
+pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 {
+    if (departing == false) { return 1000; }
+    if (ms_since_left >= DEPART_MS) { return 0; }
+    return 1000 - (ms_since_left * 1000) / DEPART_MS;
+}
+
+; --- 3. What an event does to a cell -----------------------------------------------------------------
+
+/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the
+/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is
+/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it.
+pub const FX_NONE : u8 = 0;
+pub const FX_START : u8 = 1;
+pub const FX_ACCEPT : u8 = 2;
+pub const FX_SEND_BACK : u8 = 3;
+pub const FX_LOST : u8 = 4;
+pub const FX_CANCEL : u8 = 5;
+
+/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by
+/// its verdict, cancel CANCEL; everything else nothing.
+pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0];
+
+/// `accepted`: a review's verdict was an acceptance. Only a review reads it.
+pub fn cell_effect(kind: u8, accepted: bool) -> u8 {
+    if (kind >= HIVE_KINDS) { return FX_NONE; }
+    let fx : u8 = FX_OF_KIND[kind];
+    if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; }
+    return fx;
+}
+
+/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn.
+pub const FLASH_MS : u32 = 1800;
+
+pub fn flash_alpha(ms_since: u32) -> u32 {
+    if (ms_since >= FLASH_MS) { return 0; }
+    return 1000 - (ms_since * 1000) / FLASH_MS;
+}
+
+; --- 4. How often the map may grow -------------------------------------------------------------------
+
+/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding
+/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS --
+/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of
+/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's
+/// portal (SM_PORTAL), so where it is going is already true.
+pub const CELL_BATCH_SECONDS : u32 = 300;
+
+pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool {
+    if (pending == 0) { return false; }
+    if (placed_before == false) { return true; }
+    return seconds_since_batch >= CELL_BATCH_SECONDS;
+}
+
+test a_ship_flies_where_its_bee_is {
+    // queued: at the entrance, wherever its cell stands
+    assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK;
+    assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK;
+    // claimed, its cell placed: orbit while heard, hover when quiet
+    assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT;
+    assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER;
+    // its cell not placed yet: at its repository's portal, working or not
+    assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL;
+    assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL;
+    // no cell this map can show: back at the entrance
+    assert ship_mode(false, true, CELL_NONE, false) == SM_PARK;
+    // gone from the feed: home, whatever it was
+    assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME;
+    assert ship_mode(true, false, CELL_NONE, true) == SM_HOME;
+}
+
+test a_ship_leaves_by_flying_home {
+    assert ship_kept(0) == true;
+    assert ship_kept(DEPART_MS - 1) == true;
+    assert ship_kept(DEPART_MS) == false;
+    assert ship_alpha(false, 0) == 1000;
+    assert ship_alpha(false, 99999) == 1000;
+    assert ship_alpha(true, 0) == 1000;
+    assert ship_alpha(true, 1200) == 500;
+    assert ship_alpha(true, DEPART_MS - 1) == 1;
+    assert ship_alpha(true, DEPART_MS) == 0;
+    assert ship_alpha(true, 4000000000) == 0;
+}
+
+test an_event_lights_its_cell {
+    assert cell_effect(0, false) == FX_START;
+    assert cell_effect(1, true) == FX_NONE;
+    assert cell_effect(2, false) == FX_NONE;
+    assert cell_effect(3, false) == FX_LOST;
+    assert cell_effect(4, true) == FX_NONE;
+    assert cell_effect(5, true) == FX_ACCEPT;
+    assert cell_effect(5, false) == FX_SEND_BACK;
+    assert cell_effect(6, true) == FX_NONE;
+    assert cell_effect(7, false) == FX_CANCEL;
+    assert cell_effect(8, false) == FX_NONE;
+    assert cell_effect(9, true) == FX_NONE;
+    // only a review reads the verdict
+    assert cell_effect(3, true) == FX_LOST;
+    assert cell_effect(HIVE_KINDS, true) == FX_NONE;
+}
+
+test a_flash_fades {
+    assert flash_alpha(0) == 1000;
+    assert flash_alpha(900) == 500;
+    assert flash_alpha(FLASH_MS - 1) == 1;
+    assert flash_alpha(FLASH_MS) == 0;
+    assert flash_alpha(4000000000) == 0;
+}
+
+test the_map_grows_in_batches {
+    assert cells_due(0, false, 0) == false;
+    assert cells_due(0, true, 99999) == false;
+    // the first answer places every cell at once
+    assert cells_due(3, false, 0) == true;
+    // after that, one batch a period
+    assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false;
+    assert cells_due(1, true, CELL_BATCH_SECONDS) == true;
+}
+
+invariant one_row_per_kind {
+    assert HIVE_KINDS == 10;
+    assert FX_OF_KIND[5] == FX_ACCEPT;
+    assert FLASH_MS < DEPART_MS * 2;
+    assert CELL_BATCH_SECONDS == 300;
+}
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/three-bots-deep/card.png b/apps/website/public/learn/three-bots-deep/card.png new file mode 100644 index 0000000000..99dcf91c97 Binary files /dev/null and b/apps/website/public/learn/three-bots-deep/card.png differ diff --git a/apps/website/public/learn/three-bots-deep/index.html b/apps/website/public/learn/three-bots-deep/index.html new file mode 100644 index 0000000000..8ab941ff88 --- /dev/null +++ b/apps/website/public/learn/three-bots-deep/index.html @@ -0,0 +1,436 @@ + + + + + + +Three bots deep: the Queen and the hive course, lesson 23 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Three bots deep

+

You will learn

How the hive stops agents from answering each other forever.

+

Telegram gives no loop protection between bots, so two agents could answer each other forever. Two numbers stop it. MAX_BOT_DEPTH is 3: a person, then A, B and C, then silence. BOT_REPLY_COOLDOWN_MS is 5000: a bot answers another bot at most once in 5 s. may_answer always lets a person through, and a bot only inside the chain and after the cooldown.

+

Try it

Find the gap at which agent B falls silent; then explain why C is silent too.

+

Open the interactive lesson →

+
Chain depth: human, A, B, C, then silence
Chain depth: human, A, B, C, then silence ↗
+

One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/tokens-that-burn/card.png b/apps/website/public/learn/tokens-that-burn/card.png new file mode 100644 index 0000000000..11bcddaf9d Binary files /dev/null and b/apps/website/public/learn/tokens-that-burn/card.png differ diff --git a/apps/website/public/learn/tokens-that-burn/index.html b/apps/website/public/learn/tokens-that-burn/index.html new file mode 100644 index 0000000000..dca9add8ce --- /dev/null +++ b/apps/website/public/learn/tokens-that-burn/index.html @@ -0,0 +1,214 @@ + + + + + + +Tokens that burn: the Queen and the hive course, lesson 21 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Tokens that burn

+

You will learn

When a batch of tokens burns, and which batch is spent first.

+

The owner's rule of 2026-10-01: all money goes into tokens, and tokens not spent within the month burn, so a person who paid makes content instead of saving tokens. Each batch lives MONTH_DAYS, 30 days, and days_left counts it down from the day it landed. On day 25 the package month from day 0 has 5 days left, the start gift from day 3 has 8, and a top-up from day 12 has 17. The spend order is soonest burn first.

+

Try it

Find the day the package month burns; then find the last day any batch is left.

+

Open the interactive lesson →

+
Tokens that burn: a month to spend them
Tokens that burn: a month to spend them ↗
+

Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first.

+

specs/automation/token-wallet.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a
+// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and
+// tokens that are not spent within a month burn.
+// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts
+//       (the month a package buys, what it is worth in reels and portraits, when it burns),
+//       read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx,
+//       player/src/components/Agents/PackageCards.tsx) and by render's billing
+//       (render/src/agent/billing-shared.ts: the rate and the markup every price is made with).
+// Generated: `t27c gen-ts` of this file is vendored into the host as
+//       apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports
+//       the numbers from there and never restates them; a host test runs this file's vectors
+//       against the host.
+// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent
+// within the month or they burn -- so a person who paid makes content instead of saving
+// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the
+// credits that burn soonest first, and so does this.
+// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts
+// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN).
+// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call.
+// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host
+// already charges. The month, the burn and the spend order are DECIDED but not yet in the
+// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an
+// expiry are the next change, and the burn must not be sold before it lands.
+// Owner decision still open: whether tokens bought on top of a package burn in 30 days like
+// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's
+// bought credits do (TOPUP_DAYS is the one line to change).
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::token_wallet {
+
+    pub const KIND : str = "automation";
+    pub const ID : str = "token-wallet";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 1;
+
+    // ---- One rate, one markup -----------------------------------------------------------
+    pub const TOKEN_MICRO_USD : u32 = 5000;  // one token is half a cent, everywhere
+    pub const MARKUP_PCT : u32 = 200;  // an operation is sold at twice what it costs us
+    pub const MICRO_PER_USD : u32 = 1000000;
+
+    // ---- What a month buys ----------------------------------------------------------------
+    // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is
+    // kept back, the margin is the markup on what is spent and whatever burns.
+    pub const SETUP_INCLUDES_MONTH : bool = true;  // the launch price carries the first month
+
+    // ---- The burn -----------------------------------------------------------------------
+    pub const MONTH_DAYS : u16 = 30;  // a package month burns 30 days after it lands
+    pub const TOPUP_DAYS : u16 = 30;  // owner's rule: bought tokens burn the same way
+    pub const START_DAYS : u16 = 30;  // the start gift burns too
+    pub const SPEND_ORDER : str = "soonest-burn-first";
+    pub const BURNS_BACKDATED : bool = false;  // tokens held before v1 never burn
+    pub const WARN_DAYS_FIRST : u16 = 7;
+    pub const WARN_DAYS_SECOND : u16 = 3;
+    pub const WARN_DAYS_LAST : u16 = 1;
+
+    // ---- The start gift -------------------------------------------------------------------
+    pub const START_TOKENS : u32 = 20;  // granted once, on the first paid call
+
+    // ---- What tokens are worth, in things a person can picture ----------------------------
+    // The reference reel is the story reel's own estimate (crm-story-reel.t27
+    // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the
+    // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds
+    // both to those specs, so the welcome screen cannot promise a reel at an old price.
+    pub const REEL_TOKENS : u32 = 270;
+    pub const PORTRAIT_TOKENS : u32 = 12;
+
+    // Dollars a month to tokens a month.
+    fn month_tokens(usd: u32) -> u32 {
+        return usd * MICRO_PER_USD / TOKEN_MICRO_USD;
+    }
+
+    // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so
+    // nothing is sold below twice its cost.
+    fn price_tokens(cost_micro_usd: u32) -> u32 {
+        return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD;
+    }
+
+    // Whole reels a number of tokens buys.
+    fn reels_in(tokens: u32) -> u32 {
+        return tokens / REEL_TOKENS;
+    }
+
+    // Whole portraits a number of tokens buys.
+    fn portraits_in(tokens: u32) -> u32 {
+        return tokens / PORTRAIT_TOKENS;
+    }
+
+    // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone).
+    fn days_left(granted_day: u16, today: u16, life: u16) -> u16 {
+        if (today >= granted_day + life) {
+            return 0;
+        }
+        return granted_day + life - today;
+    }
+
+    test "every dollar of a package month becomes tokens at the one rate" {
+        assert(month_tokens(15) == 3000);
+        assert(month_tokens(35) == 7000);
+        assert(month_tokens(50) == 10000);
+        assert(SETUP_INCLUDES_MONTH);
+    }
+
+    test "an operation is sold at twice its cost, rounded up" {
+        assert(MARKUP_PCT == 200);
+        assert(price_tokens(30000) == 12);
+        assert(price_tokens(20000) == 8);
+        assert(price_tokens(1) == 1);
+    }
+
+    test "a month burns on day thirty and nothing held before it burns" {
+        assert(days_left(0, 0, MONTH_DAYS) == 30);
+        assert(days_left(0, 23, MONTH_DAYS) == 7);
+        assert(days_left(0, 30, MONTH_DAYS) == 0);
+        assert(TOPUP_DAYS == MONTH_DAYS);
+        assert(SPEND_ORDER == "soonest-burn-first");
+        assert(!BURNS_BACKDATED);
+    }
+
+    test "the warnings come at seven, three and one day left" {
+        assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND);
+        assert(WARN_DAYS_SECOND > WARN_DAYS_LAST);
+        assert(WARN_DAYS_FIRST < MONTH_DAYS);
+    }
+
+    test "the start gift buys one portrait and no reel" {
+        assert(portraits_in(START_TOKENS) == 1);
+        assert(reels_in(START_TOKENS) == 0);
+    }
+
+    test "a Blogger month is twenty-five reels" {
+        assert(reels_in(month_tokens(35)) == 25);
+        assert(reels_in(month_tokens(15)) == 11);
+        assert(reels_in(month_tokens(50)) == 37);
+        assert(portraits_in(month_tokens(35)) == 583);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/twice-the-cost/card.png b/apps/website/public/learn/twice-the-cost/card.png new file mode 100644 index 0000000000..ae0783ff16 Binary files /dev/null and b/apps/website/public/learn/twice-the-cost/card.png differ diff --git a/apps/website/public/learn/twice-the-cost/index.html b/apps/website/public/learn/twice-the-cost/index.html new file mode 100644 index 0000000000..f6b834c42b --- /dev/null +++ b/apps/website/public/learn/twice-the-cost/index.html @@ -0,0 +1,214 @@ + + + + + + +Twice the cost: the Queen and the hive course, lesson 20 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Twice the cost

+

You will learn

How the project prices an operation and what a month of tokens buys.

+

Every dollar becomes tokens at one rate, TOKEN_MICRO_USD 5000: a token is half a cent. price_tokens sells an operation at twice what it costs, MARKUP_PCT 200, and rounds up so nothing sells below that: a cost of 12300 micro-dollars is 5 tokens. month_tokens turns $20 into 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12.

+

Try it

Find the costs that give exactly 1 token; then find the monthly price that buys 36 reels.

+

Open the interactive lesson →

+
The price of an operation: twice its cost, rounded up
The price of an operation: twice its cost, rounded up ↗
+

Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens.

+

specs/automation/token-wallet.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a
+// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and
+// tokens that are not spent within a month burn.
+// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts
+//       (the month a package buys, what it is worth in reels and portraits, when it burns),
+//       read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx,
+//       player/src/components/Agents/PackageCards.tsx) and by render's billing
+//       (render/src/agent/billing-shared.ts: the rate and the markup every price is made with).
+// Generated: `t27c gen-ts` of this file is vendored into the host as
+//       apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports
+//       the numbers from there and never restates them; a host test runs this file's vectors
+//       against the host.
+// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent
+// within the month or they burn -- so a person who paid makes content instead of saving
+// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the
+// credits that burn soonest first, and so does this.
+// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts
+// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN).
+// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call.
+// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host
+// already charges. The month, the burn and the spend order are DECIDED but not yet in the
+// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an
+// expiry are the next change, and the burn must not be sold before it lands.
+// Owner decision still open: whether tokens bought on top of a package burn in 30 days like
+// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's
+// bought credits do (TOPUP_DAYS is the one line to change).
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::token_wallet {
+
+    pub const KIND : str = "automation";
+    pub const ID : str = "token-wallet";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 1;
+
+    // ---- One rate, one markup -----------------------------------------------------------
+    pub const TOKEN_MICRO_USD : u32 = 5000;  // one token is half a cent, everywhere
+    pub const MARKUP_PCT : u32 = 200;  // an operation is sold at twice what it costs us
+    pub const MICRO_PER_USD : u32 = 1000000;
+
+    // ---- What a month buys ----------------------------------------------------------------
+    // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is
+    // kept back, the margin is the markup on what is spent and whatever burns.
+    pub const SETUP_INCLUDES_MONTH : bool = true;  // the launch price carries the first month
+
+    // ---- The burn -----------------------------------------------------------------------
+    pub const MONTH_DAYS : u16 = 30;  // a package month burns 30 days after it lands
+    pub const TOPUP_DAYS : u16 = 30;  // owner's rule: bought tokens burn the same way
+    pub const START_DAYS : u16 = 30;  // the start gift burns too
+    pub const SPEND_ORDER : str = "soonest-burn-first";
+    pub const BURNS_BACKDATED : bool = false;  // tokens held before v1 never burn
+    pub const WARN_DAYS_FIRST : u16 = 7;
+    pub const WARN_DAYS_SECOND : u16 = 3;
+    pub const WARN_DAYS_LAST : u16 = 1;
+
+    // ---- The start gift -------------------------------------------------------------------
+    pub const START_TOKENS : u32 = 20;  // granted once, on the first paid call
+
+    // ---- What tokens are worth, in things a person can picture ----------------------------
+    // The reference reel is the story reel's own estimate (crm-story-reel.t27
+    // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the
+    // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds
+    // both to those specs, so the welcome screen cannot promise a reel at an old price.
+    pub const REEL_TOKENS : u32 = 270;
+    pub const PORTRAIT_TOKENS : u32 = 12;
+
+    // Dollars a month to tokens a month.
+    fn month_tokens(usd: u32) -> u32 {
+        return usd * MICRO_PER_USD / TOKEN_MICRO_USD;
+    }
+
+    // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so
+    // nothing is sold below twice its cost.
+    fn price_tokens(cost_micro_usd: u32) -> u32 {
+        return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD;
+    }
+
+    // Whole reels a number of tokens buys.
+    fn reels_in(tokens: u32) -> u32 {
+        return tokens / REEL_TOKENS;
+    }
+
+    // Whole portraits a number of tokens buys.
+    fn portraits_in(tokens: u32) -> u32 {
+        return tokens / PORTRAIT_TOKENS;
+    }
+
+    // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone).
+    fn days_left(granted_day: u16, today: u16, life: u16) -> u16 {
+        if (today >= granted_day + life) {
+            return 0;
+        }
+        return granted_day + life - today;
+    }
+
+    test "every dollar of a package month becomes tokens at the one rate" {
+        assert(month_tokens(15) == 3000);
+        assert(month_tokens(35) == 7000);
+        assert(month_tokens(50) == 10000);
+        assert(SETUP_INCLUDES_MONTH);
+    }
+
+    test "an operation is sold at twice its cost, rounded up" {
+        assert(MARKUP_PCT == 200);
+        assert(price_tokens(30000) == 12);
+        assert(price_tokens(20000) == 8);
+        assert(price_tokens(1) == 1);
+    }
+
+    test "a month burns on day thirty and nothing held before it burns" {
+        assert(days_left(0, 0, MONTH_DAYS) == 30);
+        assert(days_left(0, 23, MONTH_DAYS) == 7);
+        assert(days_left(0, 30, MONTH_DAYS) == 0);
+        assert(TOPUP_DAYS == MONTH_DAYS);
+        assert(SPEND_ORDER == "soonest-burn-first");
+        assert(!BURNS_BACKDATED);
+    }
+
+    test "the warnings come at seven, three and one day left" {
+        assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND);
+        assert(WARN_DAYS_SECOND > WARN_DAYS_LAST);
+        assert(WARN_DAYS_FIRST < MONTH_DAYS);
+    }
+
+    test "the start gift buys one portrait and no reel" {
+        assert(portraits_in(START_TOKENS) == 1);
+        assert(reels_in(START_TOKENS) == 0);
+    }
+
+    test "a Blogger month is twenty-five reels" {
+        assert(reels_in(month_tokens(35)) == 25);
+        assert(reels_in(month_tokens(15)) == 11);
+        assert(reels_in(month_tokens(50)) == 37);
+        assert(portraits_in(month_tokens(35)) == 583);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/learn/who-is-called/card.png b/apps/website/public/learn/who-is-called/card.png new file mode 100644 index 0000000000..4f83e8612f Binary files /dev/null and b/apps/website/public/learn/who-is-called/card.png differ diff --git a/apps/website/public/learn/who-is-called/index.html b/apps/website/public/learn/who-is-called/index.html new file mode 100644 index 0000000000..dfa530c1ed --- /dev/null +++ b/apps/website/public/learn/who-is-called/index.html @@ -0,0 +1,436 @@ + + + + + + +Who is called: the Queen and the hive course, lesson 22 of 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiРусский
+
+ +

Who is called

+

You will learn

Which messages in the hive group wake an agent, from a person and from another agent's bot.

+

Every agent has its own bot and its own topic in one forum group. A person calls an agent by its tag, by @all, by a reply, or by writing in its topic. Another agent's bot calls it only by a tag or a reply: a bot never triggers the whole hive and never wakes a topic's owner by merely writing there. addressed in agent-hive-group.t27 is that rule, one line long.

+

Try it

Find the two ways only a person can call an agent; then turn on all four and compare the senders.

+

Open the interactive lesson →

+
Who is called: an agent bot in a group of agents
Who is called: an agent bot in a group of agents ↗
+

Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Open the lesson's spec in the player ↗

+ +

All lessons

+ +
+ + + diff --git a/apps/website/public/ru/learn/a-boundary-of-real-files/card.png b/apps/website/public/ru/learn/a-boundary-of-real-files/card.png new file mode 100644 index 0000000000..60e0490148 Binary files /dev/null and b/apps/website/public/ru/learn/a-boundary-of-real-files/card.png differ diff --git a/apps/website/public/ru/learn/a-boundary-of-real-files/index.html b/apps/website/public/ru/learn/a-boundary-of-real-files/index.html new file mode 100644 index 0000000000..2a656708bb --- /dev/null +++ b/apps/website/public/ru/learn/a-boundary-of-real-files/index.html @@ -0,0 +1,205 @@ + + + + + + +Граница из настоящих файлов: курс «Королева и улей», урок 2 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Граница из настоящих файлов

+

Вы узнаете

Какие предложенные пути задача может забронировать и почему один выдуманный путь останавливает всё оформление.

+

Граница задачи — это бронь: пока пчела работает, другие пчёлы эти файлы не берут. Поэтому неверная граница хуже отсутствующей: она держит файлы, которые работе не принадлежат. path_accepted принимает любой существующий файл, а новый — только если существует его папка, задача его упоминает и язык разрешён правилом only-t27. shape_lands возвращает оформление в задачу, только если все разделы на месте, путей от 1 до 12 и ни один не отклонён.

+

Попробовать

Сделайте два красных пути зелёными, меняя только то, что может быть правдой; затем оставьте раздел недописанным и посмотрите на ворота.

+

Открыть интерактивный урок →

+
A boundary: which files a task may reserve
A boundary: which files a task may reserve ↗
+

Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds.

+

specs/queen/shaping.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks
+; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix
+; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete.
+; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most
+; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as
+; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703,
+; #7642), which the rule counts as missing.
+; --
+; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section).
+; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a
+; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either
+; SHAPED by her (the missing sections are written by a model, every boundary path is checked against
+; the repository, and the body is written back only if judge() then returns 0), or, when shaping
+; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing.
+; --
+; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the
+; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is
+; accepted only when the issue itself cites it and its directory exists, and never when it would be a
+; new file in a language the only-t27 rule denies.
+; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenShaping;
+
+/// What the Queen does with one open issue.
+///   LEAVE  nothing: it is ready, a container, or already told
+///   SHAPE  ask for the missing sections, judge them, write them back if they pass
+///   TELL   comment once, naming what is missing
+pub const SH_LEAVE : u8 = 0;
+pub const SH_SHAPE : u8 = 1;
+pub const SH_TELL : u8 = 2;
+
+/// Attempts at shaping one issue before its author is told instead.
+pub const SHAPE_ATTEMPT_LIMIT : u32 = 2;
+/// Issues shaped per round. A shape is one model call; the round has other work.
+pub const SHAPES_PER_ROUND : u32 = 3;
+/// Minutes between two attempts on one issue, so a refused shape is not retried every round.
+pub const SHAPE_RETRY_MINUTES : u32 = 360;
+/// A boundary is a reservation; one naming more files than this is not a task, it is a project.
+pub const MAX_BOUNDARY_PATHS : u32 = 12;
+
+/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap;
+/// `attempts`: shapes tried so far; `told`: the author was already told.
+pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 {
+    if (missing == 0) { return SH_LEAVE; }
+    if (container) { return SH_LEAVE; }
+    if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; }
+    if (told) { return SH_LEAVE; }
+    return SH_TELL;
+}
+
+/// Is one proposed boundary path acceptable?
+///   exists       the file is in the repository at the base ref
+///   dir_exists   its directory is (a new file beside its siblings)
+///   cited        the issue's own title or body names this path
+///   foreign_new  it would be a NEW file whose extension the only-t27 rule denies to new work
+/// A file that exists is always acceptable: finding the file a title describes is the point. A file
+/// that does not exist must be cited by the issue, in a directory that exists, and not foreign.
+pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool {
+    if (exists) { return true; }
+    if (dir_exists == false) { return false; }
+    if (foreign_new) { return false; }
+    return cited;
+}
+
+/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and
+/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted.
+pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool {
+    if (missing_after != 0) { return false; }
+    if (paths == 0) { return false; }
+    if (paths > MAX_BOUNDARY_PATHS) { return false; }
+    return rejected == 0;
+}
+
+/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue.
+pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool {
+    if (attempts == 0) { return true; }
+    return minutes_since_last >= SHAPE_RETRY_MINUTES;
+}
+
+test a_ready_issue_and_a_container_are_left_alone {
+    assert shape_action(0, false, 0, false) == SH_LEAVE;
+    assert shape_action(15, true, 0, false) == SH_LEAVE;
+    assert shape_action(1, true, 9, false) == SH_LEAVE;
+}
+
+test an_unready_issue_is_shaped_then_its_author_is_told_once {
+    assert shape_action(1, false, 0, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE;
+    assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE;
+}
+
+test a_boundary_path_must_be_true_of_the_repository {
+    // an existing file, cited or found
+    assert path_accepted(true, true, false, false) == true;
+    assert path_accepted(true, true, true, false) == true;
+    // a new spec the issue asks for, beside its siblings
+    assert path_accepted(false, true, true, false) == true;
+    // a new file the issue never mentioned: invented
+    assert path_accepted(false, true, false, false) == false;
+    // a new file in a directory that does not exist
+    assert path_accepted(false, false, true, false) == false;
+    // a new .py or .ts the only-t27 rule denies, even when cited
+    assert path_accepted(false, true, true, true) == false;
+}
+
+test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds {
+    assert shape_lands(0, 1, 0) == true;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true;
+    assert shape_lands(1, 3, 0) == false;
+    assert shape_lands(0, 0, 0) == false;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false;
+    assert shape_lands(0, 3, 1) == false;
+}
+
+test a_refused_shape_is_not_retried_every_round {
+    assert attempt_due(0, 0) == true;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES) == true;
+}
+
+invariant the_codes_and_limits_never_move {
+    assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2;
+    assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0;
+    assert MAX_BOUNDARY_PATHS > 0;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/a-deploy-mid-flight/card.png b/apps/website/public/ru/learn/a-deploy-mid-flight/card.png new file mode 100644 index 0000000000..595a84a771 Binary files /dev/null and b/apps/website/public/ru/learn/a-deploy-mid-flight/card.png differ diff --git a/apps/website/public/ru/learn/a-deploy-mid-flight/index.html b/apps/website/public/ru/learn/a-deploy-mid-flight/index.html new file mode 100644 index 0000000000..9190f525bc --- /dev/null +++ b/apps/website/public/ru/learn/a-deploy-mid-flight/index.html @@ -0,0 +1,396 @@ + + + + + + +Деплой на лету: курс «Королева и улей», урок 9 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Деплой на лету

+

Вы узнаете

Что передеплой делает с пчелой, ревью, раундом, фоновой работой и ожиданием.

+

Каждое слияние передеплоивает Королеву, а в этот момент идёт работа. drain.t27 её сортирует. Раунд, шаг фоновой работы и строка ожидания — это строки, которые хранит база: процесс их оставляет, и следующий продолжает. Ревью останавливается: остановка теряет не больше одного ревью, и его делают заново. Пчелу держат, потому что сделанное ею живёт в её разговоре; на потолке, drain_cap_seconds, сегодня 1800 с, её возвращают, а работу сохраняют в её ветку.

+

Попробовать

Протяните ожидание от 0 до потолка и посмотрите, как меняется строка пчелы; затем сократите дренаж платформы до 1000 с.

+

Открыть интерактивный урок →

+
A deploy mid-flight: what happens to a running bee
A deploy mid-flight: what happens to a running bee ↗
+

The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store.

+

specs/queen/drain.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/drain.t27 -- when a draining Queen process may exit: what it waits for, what it stops, what it leaves
+; Owner's night loop, 2026-10-10 (gHashTag/trios#1729 item 2): "every deploy still freezes the swarm for
+; 1800 s; with durable jobs and waits and stoppable turns, a spec can bound the drain by in-flight turns".
+; Measured before this card (gHashTag/trios trios/agent-server, branch queen):
+; - the drain is not a fixed wait. On SIGTERM the server drains its own bee runner (queen-runner.ts
+;   drainBeeRunner): it takes no new order and waits while any bee it carries still runs, up to
+;   TRIOS_BEE_RUNNER_DRAIN_SECONDS = 1800, then salvages the branch of each bee still running and exits
+;   about 2 s later. Rounds, the reviewer and the wait scheduler run on through that wait
+;   (roundsThroughDrain, BrowserOS#546) and are cut by the exit. Railway's
+;   RAILWAY_DEPLOYMENT_DRAINING_SECONDS = 1830 is only the SIGKILL that follows; the service has a
+;   volume, so the next container starts when this one ends.
+; - so the Queen's own bees are what the drain waits for, and they usually outlive it. GitHub's
+;   deployment statuses for the Queen's service, 2026-10-08 23:14Z to 2026-10-10 12:01Z: 7 of 8 deploys
+;   took 30.4 to 33.7 min from start to live, and one took 2.8 min (00:44Z, a quiet night).
+; - the 1800 s protects a bee that ends within 30 min of SIGTERM. A bee still running then is cut
+;   anyway (salvaged, its conversation lost), and its row stays claimed until its task lease lapses.
+; Owner rules this card keeps (2026-10-08): a deploy must not cut a bee short, and must not idle the
+; swarm; never shorten the drain without asking (a 120 s drain was tried and reverted in 5 min).
+; How others stop for a deploy: OTP stops children in reverse order with a shutdown time each, then
+; kills; Akka Cluster Sharding hands a shard off within 60 s and warns that Kubernetes' 30 s grace can
+; be shorter; Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds, then SIGKILL; DBOS,
+; Temporal and Restate keep a workflow's progress in the store, so a stopped process loses only the
+; step it was on.
+; What is reused, not restated:
+; - actors.t27 turn_signal (a turn's own bound, 0 meaning TURN_MAX_SECONDS) and stop_signal
+;   (X_SHUTDOWN, then X_KILL past a timeout);
+; - turn_stop.t27: a stopped turn gets its abort, TURN_STOP_GRACE_MS, then its escalation, and its lane
+;   is free within LANE_FREE_MS;
+; - waits.t27 section 7: a deploy writes nothing to a wait; the next process takes it up
+;   (pickup_after_seconds);
+; - jobs.t27: a job holds no lane, and an effect runs through the effects journal, so a process that
+;   stops mid-step never repeats it;
+; - control.t27 section 2: a runner's task lease lapses TASK_LEASE_TTL_SECONDS after its last renewal,
+;   and only then is its task reclaimable.
+; Not here: the store, the signals, the salvage (the runtime, gHashTag/trios).
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDrain;
+
+use queen::actors::turn_signal;
+use queen::actors::stop_signal;
+use queen::control::reclaimable;
+use queen::waits::pickup_after_seconds;
+
+; --- 1. What a draining process holds ---------------------------------------------------------------
+
+/// The kinds of work a Queen process can hold when SIGTERM arrives.
+/// WK_BEE: a bee this process runs for its runner (queen-runner.ts), a turn of up to an hour.
+/// WK_REVIEW: one review (reviewer.t27), a turn bounded by REVIEW_ROW_SECONDS.
+/// WK_ROUND: a Queen round, a reconcile: it re-reads the store and decides again.
+/// WK_JOB: a step of a job (jobs.t27).
+/// WK_WAIT: a wait row this process claimed (waits.t27).
+pub const WK_BEE : u8 = 0;
+pub const WK_REVIEW : u8 = 1;
+pub const WK_ROUND : u8 = 2;
+pub const WK_JOB : u8 = 3;
+pub const WK_WAIT : u8 = 4;
+pub const WORK_KINDS : u8 = 5;
+
+/// actors.t27 X_KILL and X_NONE, written as literals here (stop_signal and turn_signal answer them).
+pub const X_KILL_LIT : u8 = 2;
+pub const X_NONE_LIT : u8 = 99;
+
+/// Whether the store keeps this work as it stands, so another process continues it from there and
+/// a stop throws nothing away. A wait is a row a deploy never writes (waits.t27 section 7). A job is
+/// a row and its effects are journaled (jobs.t27). A round is a reconcile: everything it decided is
+/// decided again from the store by the next round, and every effect it made is journaled. A review
+/// and a bee are turns: their row stays, but what the turn has done so far is in its process.
+pub fn row_keeps(kind: u8) -> bool {
+    if (kind == WK_WAIT) { return true; }
+    if (kind == WK_JOB) { return true; }
+    if (kind == WK_ROUND) { return true; }
+    return false;
+}
+
+/// A turn's own bound in seconds, as actors.t27 turn_signal reads it (0 is TURN_MAX_SECONDS, 3600).
+/// A review is reviewer.t27 REVIEW_ROW_SECONDS (300, a literal here). A bee names none.
+pub const REVIEW_BOUND_SECONDS : u32 = 300;
+
+pub fn bound_of(kind: u8) -> u32 {
+    if (kind == WK_REVIEW) { return REVIEW_BOUND_SECONDS; }
+    return 0;
+}
+
+/// The most work a drain may throw away to end sooner: one review. A turn whose own timer kills it
+/// within this many seconds (turn_signal says X_KILL at this age) loses at most that much to a stop,
+/// and the next process redoes it from its row. A bee's turn does not qualify: its bound is an hour,
+/// and what it has done lives in its conversation, which no row keeps. Chosen, not measured.
+pub const STOP_LOSS_SECONDS : u32 = 300;
+
+pub fn redo_is_cheap(bound_seconds: u32) -> bool {
+    return turn_signal(STOP_LOSS_SECONDS, bound_seconds) == X_KILL_LIT;
+}
+
+; --- 2. What the drain owes each piece of work --------------------------------------------------------
+
+/// DV_DONE: its work ended; nothing is owed.
+/// DV_LEAVE: the store keeps it (row_keeps); the process exits without it and the next one takes it up.
+/// DV_STOP: the drain does not wait for it. It runs on while something else holds the process, and is
+/// stopped when the process exits, through turn_stop.t27 (its abort, the grace, the escalation).
+/// DV_HOLD: the drain waits for it to end on its own.
+/// DV_HAND_BACK: held to the cap and still running: it is stopped as DV_STOP is, what it wrote is
+/// salvaged into its branch and stored, and its row is ended now, so its issue is free at once.
+pub const DV_DONE : u8 = 0;
+pub const DV_LEAVE : u8 = 1;
+pub const DV_STOP : u8 = 2;
+pub const DV_HOLD : u8 = 3;
+pub const DV_HAND_BACK : u8 = 4;
+
+/// A bee is held until it ends or the cap passes: the owner's rule that a deploy does not cut a bee
+/// short, kept to the same cap as before this card. A review is not held: a stop loses at most one
+/// review, which the next process redoes. Rows are left.
+pub fn drain_verdict(kind: u8, ended: bool, waited_seconds: u32, cap_seconds: u32) -> u8 {
+    if (ended) { return DV_DONE; }
+    if (row_keeps(kind)) { return DV_LEAVE; }
+    if (redo_is_cheap(bound_of(kind))) { return DV_STOP; }
+    if (waited_seconds >= cap_seconds) { return DV_HAND_BACK; }
+    return DV_HOLD;
+}
+
+; --- 3. What keeps working while something holds ------------------------------------------------------
+
+/// Whether a source of work starts anything new during the drain. Nothing that would start a bee in
+/// this process: that bee would hold the drain for up to its hour. Everything else keeps going while
+/// a held bee keeps the process alive, because the next process cannot start before this one ends
+/// (one instance, one volume), and stopping the rounds then idled the swarm for the whole drain
+/// (measured 2026-10-08, BrowserOS#546). Once nothing holds, the process is leaving: nothing starts.
+pub fn takes_new_work(starts_a_bee_here: bool, holding: u32) -> bool {
+    if (starts_a_bee_here) { return false; }
+    return holding > 0;
+}
+
+; --- 4. The hard cap ----------------------------------------------------------------------------------
+
+/// What must still fit after the cap, before the platform's SIGKILL: the hand-back of every held bee
+/// (its abort and grace, the salvage into its branch, the bundle, the row), run side by side under one
+/// deadline, then the server's own close (main.ts gives it 2 s). Chosen, not measured.
+pub const HAND_BACK_SECONDS : u32 = 20;
+pub const EXIT_SECONDS : u32 = 5;
+
+/// The cap: the runner's drain (TRIOS_BEE_RUNNER_DRAIN_SECONDS), never so late that the hand-back and
+/// the exit cannot finish before the platform kills the process (RAILWAY_DEPLOYMENT_DRAINING_SECONDS).
+/// A platform drain of 0 is one this process cannot read: the runner's drain stands, as before.
+/// With today's 1800 and 1830 the cap is 1800: this card cuts no bee sooner than the drain it replaces.
+pub fn drain_cap_seconds(runner_drain_seconds: u32, platform_seconds: u32) -> u32 {
+    if (platform_seconds == 0) { return runner_drain_seconds; }
+    if (platform_seconds <= HAND_BACK_SECONDS + EXIT_SECONDS) { return 0; }
+    let latest : u32 = platform_seconds - HAND_BACK_SECONDS - EXIT_SECONDS;
+    if (runner_drain_seconds < latest) { return runner_drain_seconds; }
+    return latest;
+}
+
+; --- 5. The exit --------------------------------------------------------------------------------------
+
+/// turn_stop.t27's promises, written as literals here: the grace after the abort, and the time by
+/// which a stopped turn's lane is free.
+pub const TURN_STOP_GRACE_MS : u32 = 3000;
+pub const LANE_FREE_MS : u32 = 5000;
+
+pub const EXIT_NOW : u8 = 0;
+pub const EXIT_WAIT : u8 = 1;
+
+/// The process exits once nothing holds it and every turn it stopped has had its stop land: its abort
+/// at once, the grace, the escalation (stop_signal with the grace says X_KILL by LANE_FREE_MS). A
+/// process that stopped nothing exits at once.
+pub fn exit_step(holding: u32, stopping: u32, since_stop_ms: u32) -> u8 {
+    if (holding > 0) { return EXIT_WAIT; }
+    if (stopping == 0) { return EXIT_NOW; }
+    if (since_stop_ms >= LANE_FREE_MS) { return EXIT_NOW; }
+    return EXIT_WAIT;
+}
+
+; --- 6. When the work is free for another process -----------------------------------------------------
+
+/// control.t27 TASK_LEASE_TTL_SECONDS, written as a literal here (pinned by the invariant below).
+pub const TASK_LEASE_TTL_LIT : u32 = 180;
+
+/// Seconds after the exit before another process may take the issue of a bee the drain stopped.
+/// Handed back, its row is ended at the cap: free at once. Left claimed, as before this card, it waits
+/// until its task lease lapses (control.t27 reclaimable), counted from the last renewal, which stopped
+/// with the process.
+pub fn bee_free_after_seconds(handed_back: bool, since_renewal: u32) -> u32 {
+    if (handed_back) { return 0; }
+    if (reclaimable(false, since_renewal)) { return 0; }
+    return TASK_LEASE_TTL_LIT - since_renewal;
+}
+
+/// Seconds after the exit before another process may take a wait row this process had claimed: the
+/// rest of its claim (waits.t27 pickup_after_seconds). The drain writes nothing to it.
+pub fn wait_free_after_seconds(claimed: bool, since_claim: u32) -> u32 {
+    return pickup_after_seconds(claimed, since_claim);
+}
+
+; --- tests --------------------------------------------------------------------------------------------
+
+test rows_are_left_to_the_next_process {
+    assert row_keeps(WK_WAIT) == true;
+    assert row_keeps(WK_JOB) == true;
+    assert row_keeps(WK_ROUND) == true;
+    assert row_keeps(WK_REVIEW) == false;
+    assert row_keeps(WK_BEE) == false;
+    assert drain_verdict(WK_WAIT, false, 0, 1800) == DV_LEAVE;
+    assert drain_verdict(WK_JOB, false, 0, 1800) == DV_LEAVE;
+    assert drain_verdict(WK_ROUND, false, 0, 1800) == DV_LEAVE;
+    // past the cap too: a row is never handed back, it is simply there
+    assert drain_verdict(WK_WAIT, false, 1800, 1800) == DV_LEAVE;
+}
+
+test a_review_is_stopped_at_the_exit_not_waited_for {
+    assert bound_of(WK_REVIEW) == 300;
+    assert redo_is_cheap(300) == true;
+    assert redo_is_cheap(301) == false;
+    assert drain_verdict(WK_REVIEW, false, 0, 1800) == DV_STOP;
+    assert drain_verdict(WK_REVIEW, false, 1800, 1800) == DV_STOP;
+}
+
+test a_bee_is_held_until_it_ends_or_the_cap {
+    assert bound_of(WK_BEE) == 0;
+    // its bound is TURN_MAX_SECONDS: a stop could throw away an hour
+    assert redo_is_cheap(0) == false;
+    assert redo_is_cheap(3600) == false;
+    assert drain_verdict(WK_BEE, false, 0, 1800) == DV_HOLD;
+    assert drain_verdict(WK_BEE, false, 1799, 1800) == DV_HOLD;
+    assert drain_verdict(WK_BEE, false, 1800, 1800) == DV_HAND_BACK;
+    assert drain_verdict(WK_BEE, false, 4000, 1800) == DV_HAND_BACK;
+    // a bee that ended is owed nothing, at any time
+    assert drain_verdict(WK_BEE, true, 0, 1800) == DV_DONE;
+    assert drain_verdict(WK_BEE, true, 1800, 1800) == DV_DONE;
+    assert drain_verdict(WK_REVIEW, true, 0, 1800) == DV_DONE;
+    assert drain_verdict(WK_WAIT, true, 0, 1800) == DV_DONE;
+}
+
+test a_cap_of_zero_hands_every_bee_back_at_once {
+    assert drain_verdict(WK_BEE, false, 0, 0) == DV_HAND_BACK;
+}
+
+test nothing_starts_a_bee_here_and_the_rest_runs_only_while_held {
+    assert takes_new_work(true, 3) == false;
+    assert takes_new_work(true, 0) == false;
+    assert takes_new_work(false, 1) == true;
+    assert takes_new_work(false, 0) == false;
+}
+
+test the_cap_is_todays_drain_and_fits_the_platform {
+    // today: runner 1800, Railway 1830 -> 1800, no bee cut sooner than before
+    assert drain_cap_seconds(1800, 1830) == 1800;
+    // a platform drain too short for the hand-back moves the cap earlier
+    assert drain_cap_seconds(1800, 1800) == 1775;
+    assert drain_cap_seconds(1800, 26) == 1;
+    assert drain_cap_seconds(1800, 25) == 0;
+    assert drain_cap_seconds(1800, 10) == 0;
+    // a shorter runner drain is the owner's to choose, and it stands
+    assert drain_cap_seconds(600, 1830) == 600;
+    assert drain_cap_seconds(0, 1830) == 0;
+    // a platform drain this process cannot read leaves the runner's drain alone
+    assert drain_cap_seconds(1800, 0) == 1800;
+}
+
+test the_exit_waits_for_held_work_and_for_stops_to_land {
+    assert exit_step(1, 0, 0) == EXIT_WAIT;
+    assert exit_step(2, 3, 9000) == EXIT_WAIT;
+    assert exit_step(0, 0, 0) == EXIT_NOW;
+    assert exit_step(0, 2, 0) == EXIT_WAIT;
+    assert exit_step(0, 2, 4999) == EXIT_WAIT;
+    assert exit_step(0, 2, 5000) == EXIT_NOW;
+}
+
+test a_handed_back_bee_is_free_at_once_and_a_left_one_waits_out_its_lease {
+    assert bee_free_after_seconds(true, 0) == 0;
+    assert bee_free_after_seconds(true, 100) == 0;
+    assert bee_free_after_seconds(false, 0) == 180;
+    assert bee_free_after_seconds(false, 45) == 135;
+    assert bee_free_after_seconds(false, 179) == 1;
+    assert bee_free_after_seconds(false, 180) == 0;
+    assert bee_free_after_seconds(false, 4294967295) == 0;
+}
+
+test a_wait_row_is_free_after_the_rest_of_its_claim {
+    assert wait_free_after_seconds(false, 0) == 0;
+    assert wait_free_after_seconds(true, 0) == 60;
+    assert wait_free_after_seconds(true, 45) == 15;
+    assert wait_free_after_seconds(true, 60) == 0;
+}
+
+invariant the_verdicts_and_kinds_are_pinned {
+    assert DV_DONE == 0;
+    assert DV_LEAVE == 1;
+    assert DV_STOP == 2;
+    assert DV_HOLD == 3;
+    assert DV_HAND_BACK == 4;
+    assert WK_BEE == 0;
+    assert WK_WAIT == 4;
+    assert WORK_KINDS == 5;
+    assert EXIT_NOW == 0;
+    assert EXIT_WAIT == 1;
+}
+
+invariant the_literals_match_the_cards_they_come_from {
+    // actors.t27: turn_signal answers X_KILL at the bound and X_NONE before it
+    assert turn_signal(300, 300) == X_KILL_LIT;
+    assert turn_signal(299, 300) == X_NONE_LIT;
+    // control.t27: the task lease lapses at TASK_LEASE_TTL_SECONDS
+    assert reclaimable(false, TASK_LEASE_TTL_LIT) == true;
+    assert reclaimable(false, TASK_LEASE_TTL_LIT - 1) == false;
+    // turn_stop.t27: by LANE_FREE_MS a stopped turn's grace has passed (stop_signal says X_KILL)
+    assert stop_signal(LANE_FREE_MS, TURN_STOP_GRACE_MS) == X_KILL_LIT;
+    assert stop_signal(TURN_STOP_GRACE_MS - 1, TURN_STOP_GRACE_MS) == X_NONE_LIT;
+}
+
+invariant a_review_is_cheap_to_redo_and_a_bee_is_not {
+    assert STOP_LOSS_SECONDS == REVIEW_BOUND_SECONDS;
+    assert redo_is_cheap(REVIEW_BOUND_SECONDS) == true;
+    assert redo_is_cheap(0) == false;
+}
+
+invariant the_hand_back_fits_between_todays_drain_and_the_platform_kill {
+    assert HAND_BACK_SECONDS + EXIT_SECONDS <= 30;
+    assert drain_cap_seconds(1800, 1830) == 1800;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/a-key-rests/card.png b/apps/website/public/ru/learn/a-key-rests/card.png new file mode 100644 index 0000000000..ef7bfe3250 Binary files /dev/null and b/apps/website/public/ru/learn/a-key-rests/card.png differ diff --git a/apps/website/public/ru/learn/a-key-rests/index.html b/apps/website/public/ru/learn/a-key-rests/index.html new file mode 100644 index 0000000000..7d602356c9 --- /dev/null +++ b/apps/website/public/ru/learn/a-key-rests/index.html @@ -0,0 +1,458 @@ + + + + + + +Ключ отдыхает: курс «Королева и улей», урок 17 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Ключ отдыхает

+

Вы узнаете

Сколько отклонённый ключ отдыхает, прежде чем пчела сможет взять его снова.

+

Временный отказ даёт ключу отдохнуть COOL_BASE_SECONDS, 15 с, вдвое больше за каждый отказ подряд и не больше COOL_CAP_SECONDS, 240 с: 15, 30, 60, 120, 240. Исчерпанная квота или отклонённый ключ закрывают его на KEY_BREAK_SECONDS, 1800 с. Всё это даёт cooldown_seconds. Отдых короткий, чтобы 34 ключа никогда не отдыхали все сразу во время минутного всплеска, как на 2026-10-17.

+

Попробовать

Найдите число отказов, после которого отдых перестаёт расти; затем переключите на квоту и прочитайте отдых.

+

Открыть интерактивный урок →

+
How long a refused key rests
How long a refused key rests ↗
+

A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/a-refused-key/card.png b/apps/website/public/ru/learn/a-refused-key/card.png new file mode 100644 index 0000000000..d95da0b877 Binary files /dev/null and b/apps/website/public/ru/learn/a-refused-key/card.png differ diff --git a/apps/website/public/ru/learn/a-refused-key/index.html b/apps/website/public/ru/learn/a-refused-key/index.html new file mode 100644 index 0000000000..ec47e7b8dc --- /dev/null +++ b/apps/website/public/ru/learn/a-refused-key/index.html @@ -0,0 +1,458 @@ + + + + + + +Отклонённый ключ: курс «Королева и улей», урок 16 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Отклонённый ключ

+

Вы узнаете

Как Королева сортирует отказ провайдера: временный, квота, доступ или неверный запрос.

+

Пчёлы обращаются к моделям по ключам, и ключи отклоняют. key_failover.t27 сортирует каждый отказ функцией error_kind. Код z.ai в теле ответа важнее статуса HTTP, потому что z.ai отвечает одним и тем же 429 и на исчерпанную квоту, и на временную перегрузку: код 1305 временный, 1113 — квота. Отсутствие ответа — тоже временный отказ. Отказ по квоте или доступу закрывает ключ, а в счёт задачи идёт только неверный запрос.

+

Попробовать

Сравните 429 с кодами 1308 и 1312; затем прочитайте, что значит отсутствие ответа.

+

Открыть интерактивный урок →

+
A refused key: transient, quota, auth or a bad request
A refused key: transient, quota, auth or a bad request ↗
+

Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/a-tasks-life/card.png b/apps/website/public/ru/learn/a-tasks-life/card.png new file mode 100644 index 0000000000..846236f43a Binary files /dev/null and b/apps/website/public/ru/learn/a-tasks-life/card.png differ diff --git a/apps/website/public/ru/learn/a-tasks-life/index.html b/apps/website/public/ru/learn/a-tasks-life/index.html new file mode 100644 index 0000000000..6aad60877d --- /dev/null +++ b/apps/website/public/ru/learn/a-tasks-life/index.html @@ -0,0 +1,669 @@ + + + + + + +Жизнь задачи: курс «Королева и улей», урок 7 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Жизнь задачи

+

Вы узнаете

Через какие состояния проходит запущенная задача и какие события допустимы в каждом.

+

dispatch.t27 — жизненный цикл задачи у Королевы, цикл, который выбирает, запускает, завершает, проверяет и отпускает работу. У задачи 12 состояний и 16 событий. next_state отвечает на каждую пару, а 99 значит, что событие в этом состоянии недопустимо: вердикт не доходит до работающей задачи, а снятие — до завершённой. После вердикта возврат ждёт свой порог и отпускается; даже принятую работу возвращают, если CI краснеет ниже потолка.

+

Попробовать

Проведите задачу от none до accept; затем сделайте CI красным на потолке и найдите выход, который остаётся.

+

Открыть интерактивный урок →

+
A task's life: the Queen's state machine
A task's life: the Queen's state machine ↗
+

Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out.

+

specs/queen/dispatch.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done
+; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written
+; down in this family of repositories and only one of them moves work: the round of the trios supervisor
+; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it,
+; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with
+; the other cycles named as adapters and their gaps stated. Every decision below is a function the C
+; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py):
+;   ts      its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the
+;           public board's column, the single-flight round gate
+;   queend  its Swift policy binary, built at the pin and asked choose, review and retry
+;   pg      PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch
+;           writers on real rows, the criteria release, the CI take-back, the round's own SELECT
+;           (17.7 at this pin; the run at the old pin c25e1b02 used 16.15)
+;   lotus   gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which
+;           this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin
+;           c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host)
+;   live    one snapshot of the public endpoints of the deployed supervisor
+; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch
+; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read
+; from the source and named below: runner lanes (a claim in state queued that takes no worker slot,
+; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees)
+; and the public card's verdict and judgedHead.
+; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a
+; revision other than the pin. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDispatch;
+
+pub const KIND : str = "queen-dispatch";
+pub const ID : str = "queen/dispatch";
+pub const NAME : str = "The Queen's task lifecycle, from candidate to done";
+pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS";
+pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size";
+pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef";
+pub const CONSUMER_REPO : str = "gHashTag/trinity";
+pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
+pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530";
+pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149";
+; --- The decision: one canonical machine, the others are adapters ---------------------------------
+pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)";
+pub const ADAPTERS : [5]str = [
+    "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend",
+    "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design",
+    "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing",
+    "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task",
+    "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here"
+];
+pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state";
+pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"];
+; --- States of a dispatch row, and the events that move it ----------------------------------------
+pub const S_NONE : u32 = 0;
+pub const S_RUNNING : u32 = 1;
+pub const S_REFUSED : u32 = 2;
+pub const S_ENDED : u32 = 3;
+pub const S_WAIT : u32 = 4;
+pub const S_ACCEPT : u32 = 5;
+pub const S_SENDBACK : u32 = 6;
+pub const S_ESCALATE : u32 = 7;
+pub const S_EMPTY : u32 = 8;
+pub const S_STALE : u32 = 9;
+pub const S_REAPED : u32 = 10;
+pub const S_RELEASED : u32 = 11;
+pub const ILLEGAL : u32 = 99;
+pub const E_START : u32 = 0;
+pub const E_REFUSE : u32 = 1;
+pub const E_END : u32 = 2;
+pub const E_END_OTHER_TURN : u32 = 3;
+pub const E_REAP_STALL : u32 = 4;
+pub const E_REAP_BOOT : u32 = 5;
+pub const E_VERDICT_ACCEPT : u32 = 6;
+pub const E_VERDICT_SENDBACK : u32 = 7;
+pub const E_VERDICT_ESCALATE : u32 = 8;
+pub const E_VERDICT_WAIT : u32 = 9;
+pub const E_VERDICT_EMPTY : u32 = 10;
+pub const E_CRITERIA_CHANGED : u32 = 11;
+pub const E_CI_RED_UNDER_CEILING : u32 = 12;
+pub const E_CI_RED_AT_CEILING : u32 = 13;
+pub const E_FLOOR_PASSED : u32 = 14;
+pub const E_REDISPATCH : u32 = 15;
+pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)";
+pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation";
+pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it";
+; --- Claims: what a row costs its issue ------------------------------------------------------------
+pub const V_NONE : u32 = 0;
+pub const V_BLANK : u32 = 1;
+pub const V_WAIT : u32 = 2;
+pub const V_EMPTY : u32 = 3;
+pub const V_SENDBACK : u32 = 4;
+pub const V_ESCALATE : u32 = 5;
+pub const V_ACCEPT : u32 = 6;
+pub const V_FAILED : u32 = 7;
+pub const V_CANCELLED : u32 = 8;
+pub const V_STALE : u32 = 9;
+pub const V_OTHER : u32 = 10;
+pub const C_RUNNING : u32 = 0;
+pub const C_ACCEPTED : u32 = 1;
+pub const C_REJECTED : u32 = 2;
+pub const C_AWAITING_REVIEW : u32 = 3;
+pub const C_FAILED : u32 = 4;
+pub const EMPTY_FLOOR_MS : u64 = 1800000;
+pub const SENDBACK_FLOOR_MS : u64 = 3600000;
+pub const WAIT_FLOOR_MS : u64 = 21600000;
+pub const CEILING_RELEASE_MS : u64 = 3600000;
+pub const MAX_CEILING_RELEASES : u32 = 1;
+pub const SENDBACK_CEILING : u32 = 2;
+pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it";
+; --- Choosing ----------------------------------------------------------------------------------------
+pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates";
+pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies";
+pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing";
+pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only";
+pub const DEFAULT_WORKER_LIMIT : u32 = 4;
+pub const NONE_CHOSEN : u32 = 5;
+pub const COL_BACKLOG : u32 = 0;
+pub const COL_BLOCKED : u32 = 1;
+pub const COL_RUNNING : u32 = 2;
+pub const COL_REVIEW : u32 = 3;
+pub const COL_DONE : u32 = 4;
+pub const COL_DROPPED : u32 = 5;
+; --- Leases ------------------------------------------------------------------------------------------
+pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced";
+pub const LEASE_TTL_SECONDS : u32 = 180;
+pub const LEASE_HEARTBEAT_SECONDS : u32 = 60;
+pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane";
+pub const STALL_MINUTES : u32 = 120;
+; --- Review, retries, evidence, cancellation --------------------------------------------------------
+pub const R_ACCEPT : u32 = 0;
+pub const R_SENDBACK : u32 = 1;
+pub const R_ESCALATE : u32 = 2;
+pub const R_WAIT : u32 = 3;
+pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)";
+pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container";
+pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists";
+pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict";
+pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped";
+; --- Events, persistence, idempotency, projections --------------------------------------------------
+pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)";
+pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round";
+pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"];
+pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board";
+
+; --- End reasons for runs that produce no commits -----------------------------------------------------------
+; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted.
+; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days.
+pub const R_MODEL_ENDED_TURN : u32 = 0;      // Model ended its turn without editing any files
+pub const R_TIME_TOKEN_LIMIT : u32 = 1;     // Hit time or token limit during execution
+pub const R_CRASHED : u32 = 2;             // Container or tool crashed (with exit code)
+pub const R_BOUNDARY_CORRECT : u32 = 3;     // Boundary file was already correct, no changes needed
+pub const R_REFUSAL_START : u32 = 4;       // Refusal before the run started (invalid task, missing resources)
+pub const R_PUSH_FAILED : u32 = 5;          // Push to remote failed after successful run
+pub const R_UNKNOWN : u32 = 6;             // Cause could not be determined by the runner
+
+pub const END_REASON_COUNT : u32 = 7;
+pub const END_REASON_NAMES : [7]str = [
+    "model_ended_turn",
+    "time_token_limit", 
+    "crashed",
+    "boundary_correct",
+    "refusal_start",
+    "push_failed",
+    "unknown"
+];
+
+; --- Public board extension: show end reason on empty cards -----------------------------------------------
+; The public board's "empty" cards now include the end reason when available, enabling breakdown of
+; the 56.0% empty-branch share by reason from public endpoints alone.
+pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available";
+pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints";
+; --- Agents and workers ---------------------------------------------------------------------------------
+pub const AGENT_LETTERS : u32 = 27;
+pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27";
+; --- The specs this one sits beside ----------------------------------------------------------------------
+pub const NEIGHBOURS : [6]str = [
+    "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master",
+    "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort",
+    "specs/queen/brain_summaries.t27: does not compile in C at master",
+    "specs/brain/brain.t27: no function; does not compile in C at master",
+    "specs/brain/bus.t27: compiles, one test of a version constant",
+    "specs/brain/cognitive_loop.t27: compiles, one test of a version constant"
+];
+; --- Findings: measured, the tag names the evidence ------------------------------------------------------
+pub const FINDINGS_COUNT : u32 = 15;
+pub const FINDINGS : [15]str = [
+    "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]",
+    "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]",
+    "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]",
+    "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]",
+    "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]",
+    "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]",
+    "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]",
+    "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]",
+    "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]",
+    "f59 nothing cancels a running bee [source]",
+    "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]",
+    "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]",
+    "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]",
+    "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]",
+    "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]"
+];
+pub const ENABLED : bool = true;
+
+; --- Rules ------------------------------------------------------------------------------------------------------
+
+pub fn next_state(state: u32, event: u32) -> u32 {
+    if (state == S_NONE) {
+        if (event == E_START) { return S_RUNNING; }
+        if (event == E_REFUSE) { return S_REFUSED; }
+        return ILLEGAL;
+    }
+    if (state == S_RUNNING) {
+        if (event == E_END) { return S_ENDED; }
+        if (event == E_REAP_STALL) { return S_REAPED; }
+        if (event == E_REAP_BOOT) { return S_REAPED; }
+        return ILLEGAL;
+    }
+    if (state == S_ENDED or state == S_WAIT) {
+        if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; }
+        if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; }
+        if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; }
+        if (event == E_VERDICT_WAIT) { return S_WAIT; }
+        if (event == E_VERDICT_EMPTY) { return S_EMPTY; }
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ACCEPT) {
+        if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; }
+        if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; }
+        return ILLEGAL;
+    }
+    if (state == S_SENDBACK or state == S_EMPTY) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ESCALATE) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        return ILLEGAL;
+    }
+    if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) {
+        if (event == E_REDISPATCH) { return S_RUNNING; }
+        return ILLEGAL;
+    }
+    return ILLEGAL;
+}
+
+pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 {
+    if (finished == false) { return C_RUNNING; }
+    if (verdict == V_ACCEPT) { return C_ACCEPTED; }
+    if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; }
+    if (verdict == V_EMPTY) {
+        if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_STALE) { return C_FAILED; }
+    if (verdict == V_SENDBACK) {
+        if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+        if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; }
+        if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) {
+        if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+    }
+    return C_AWAITING_REVIEW;
+}
+
+; What the round and the board actually pass for `releases`: neither selects the column.
+pub fn releases_read_by_round(stored: u32) -> u32 {
+    return 0;
+}
+
+pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 {
+    if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; }
+    if (claim == C_RUNNING) { return COL_RUNNING; }
+    if (claim == C_ACCEPTED) { return COL_DONE; }
+    if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; }
+    return COL_DROPPED;
+}
+
+pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 {
+    if (e0) { return 0; }
+    if (e1) { return 1; }
+    if (e2) { return 2; }
+    if (e3) { return 3; }
+    if (e4) { return 4; }
+    return NONE_CHOSEN;
+}
+
+pub fn can_start(running: u32, limit: u32) -> bool {
+    return running < limit;
+}
+
+pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 {
+    if (total == 0) { return R_ESCALATE; }
+    if (judged < total) { return R_WAIT; }
+    if (met == total) {
+        if (committed_known and committed > 0) { return R_ACCEPT; }
+        return R_ESCALATE;
+    }
+    if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; }
+    return R_SENDBACK;
+}
+
+pub fn retry_allowed(real_attempts: u32) -> bool {
+    return real_attempts < SENDBACK_CEILING;
+}
+
+pub fn counts_against_issue(interrupted: bool) -> bool {
+    return interrupted == false;
+}
+
+pub fn lease_taken(expired: bool, same_holder: bool) -> bool {
+    return expired or same_holder;
+}
+
+pub fn fence_after(fence: u64) -> u64 {
+    return fence + 1;
+}
+
+pub fn owners_per_term() -> u32 {
+    return 1;
+}
+
+pub fn stall_reapable(age_minutes: u32) -> bool {
+    return age_minutes > STALL_MINUTES;
+}
+
+pub fn followup_rounds(requests_during_round: u32) -> u32 {
+    if (requests_during_round > 0) { return 1; }
+    return 0;
+}
+
+pub fn accept_requires_pull_request() -> bool {
+    return false;
+}
+
+pub fn writer_enforces_table() -> bool {
+    return false;
+}
+
+invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; }
+invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; }
+invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; }
+invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; }
+invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); }
+invariant all_end_reasons_valid { 
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN);
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT);
+    assert is_valid_end_reason(R_CRASHED);
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT);
+    assert is_valid_end_reason(R_REFUSAL_START);
+    assert is_valid_end_reason(R_PUSH_FAILED);
+    assert is_valid_end_reason(R_UNKNOWN);
+}
+
+; --- End reason handling functions --------------------------------------------------------------------------
+
+pub fn is_valid_end_reason(reason: u32) -> bool {
+    return reason < END_REASON_COUNT;
+}
+
+pub fn end_reason_to_name(reason: u32) -> str {
+    if (is_valid_end_reason(reason)) {
+        return END_REASON_NAMES[reason];
+    }
+    return "invalid_end_reason";
+}
+
+pub fn is_no_diff_end_reason(reason: u32) -> bool {
+    return reason == R_MODEL_ENDED_TURN 
+        or reason == R_TIME_TOKEN_LIMIT 
+        or reason == R_BOUNDARY_CORRECT 
+        or reason == R_REFUSAL_START 
+        or reason == R_PUSH_FAILED 
+        or reason == R_UNKNOWN;
+}
+
+pub fn is_crash_end_reason(reason: u32) -> bool {
+    return reason == R_CRASHED;
+}
+
+test the_table_moves_only_along_its_guards {
+    assert next_state(S_NONE, E_START) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END) == S_ENDED;
+    assert next_state(S_ENDED, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE;
+    assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL;
+    assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL;
+    // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886).
+    assert next_state(S_NONE, E_REFUSE) == S_REFUSED;
+    assert next_state(S_NONE, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED;
+    assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED;
+    assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK;
+    assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE;
+    assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT;
+    assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY;
+    assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK;
+    assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_START) == ILLEGAL;
+    assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING;
+}
+
+test a_claim_follows_the_verdict_and_three_clocks {
+    assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING;
+    assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED;
+    assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED;
+    assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW;
+    // #9886: each verdict that frees the issue, the send-back floor at its edge with a release
+    // already spent, and the wait floor's two edges.
+    assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED;
+    assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW;
+    assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW;
+}
+
+test a_closed_issue_is_drawn_done_whatever_its_verdict {
+    assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE;
+    assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW;
+    assert column_of(C_RUNNING, false, true) == COL_RUNNING;
+    assert column_of(C_FAILED, true, true) == COL_DROPPED;
+    assert column_of(C_ACCEPTED, true, true) == COL_DONE;
+}
+
+test the_first_eligible_wins_in_any_order {
+    assert chosen_of(false, false, true, true, false) == 2;
+    assert chosen_of(false, true, false, false, true) == 1;
+    assert chosen_of(false, false, false, false, false) == NONE_CHOSEN;
+    assert chosen_of(true, true, false, false, false) == 0;
+    assert chosen_of(false, false, false, true, true) == 3;
+    assert chosen_of(false, false, false, false, true) == 4;
+    assert can_start(3, DEFAULT_WORKER_LIMIT) == true;
+    assert can_start(4, DEFAULT_WORKER_LIMIT) == false;
+}
+
+test review_escalates_without_work_and_after_two_send_backs {
+    assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE;
+    assert review_of(2, 1, 1, true, 1, 0) == R_WAIT;
+    assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT;
+    assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK;
+    assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE;
+    assert retry_allowed(1) == true;
+    assert retry_allowed(2) == false;
+    assert counts_against_issue(true) == false;
+    assert counts_against_issue(false) == true;
+}
+
+test one_lease_one_owner_and_age_alone_reaps {
+    assert lease_taken(false, false) == false;
+    assert lease_taken(true, false) == true;
+    assert lease_taken(false, true) == true;
+    assert fence_after(3) == 4;
+    assert owners_per_term() == 1;
+    assert stall_reapable(121) == true;
+    assert stall_reapable(119) == false;
+    // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself.
+    assert stall_reapable(120) == false;
+    assert followup_rounds(5) == 1;
+    assert followup_rounds(0) == 0;
+    assert accept_requires_pull_request() == false;
+    assert writer_enforces_table() == false;
+}
+
+; --- End reason tests --------------------------------------------------------------------------------------
+
+test end_reason_constants_are_correct {
+    assert R_MODEL_ENDED_TURN == 0;
+    assert R_TIME_TOKEN_LIMIT == 1;
+    assert R_CRASHED == 2;
+    assert R_BOUNDARY_CORRECT == 3;
+    assert R_REFUSAL_START == 4;
+    assert R_PUSH_FAILED == 5;
+    assert R_UNKNOWN == 6;
+    assert END_REASON_COUNT == 7;
+    // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two
+    // call the functions that read the constants: the last reason is named, one past it is not.
+    assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown";
+    assert is_valid_end_reason(END_REASON_COUNT) == false;
+}
+
+test end_reason_names_array_matches_constants {
+    assert END_REASON_NAMES[0] == "model_ended_turn";
+    assert END_REASON_NAMES[1] == "time_token_limit";
+    assert END_REASON_NAMES[2] == "crashed";
+    assert END_REASON_NAMES[3] == "boundary_correct";
+    assert END_REASON_NAMES[4] == "refusal_start";
+    assert END_REASON_NAMES[5] == "push_failed";
+    assert END_REASON_NAMES[6] == "unknown";
+}
+
+test end_reason_validation_works {
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_valid_end_reason(R_CRASHED) == true;
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_valid_end_reason(R_REFUSAL_START) == true;
+    assert is_valid_end_reason(R_PUSH_FAILED) == true;
+    assert is_valid_end_reason(R_UNKNOWN) == true;
+    assert is_valid_end_reason(7) == false;  // One past the last valid reason
+    assert is_valid_end_reason(99) == false; // Way past the last valid reason
+}
+
+test end_reason_to_name_conversion {
+    assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn";
+    assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit";
+    assert end_reason_to_name(R_CRASHED) == "crashed";
+    assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct";
+    assert end_reason_to_name(R_REFUSAL_START) == "refusal_start";
+    assert end_reason_to_name(R_PUSH_FAILED) == "push_failed";
+    assert end_reason_to_name(R_UNKNOWN) == "unknown";
+    assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string
+}
+
+test no_diff_end_reason_classification {
+    assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_no_diff_end_reason(R_REFUSAL_START) == true;
+    assert is_no_diff_end_reason(R_PUSH_FAILED) == true;
+    assert is_no_diff_end_reason(R_UNKNOWN) == true;
+    assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category
+}
+
+test crash_end_reason_classification {
+    assert is_crash_end_reason(R_CRASHED) == true;
+    assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false;
+    assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false;
+    assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false;
+    assert is_crash_end_reason(R_REFUSAL_START) == false;
+    assert is_crash_end_reason(R_PUSH_FAILED) == false;
+    assert is_crash_end_reason(R_UNKNOWN) == false;
+}
+
+test end_reason_enables_breakdown_of_empty_branches {
+    // This test validates that the end reason vocabulary enables breaking down
+    // the 56.0% empty-branch share by reason from public endpoints alone
+    
+    // Simulate counts for empty branches by reason
+    var model_ended_count : u32 = 100;
+    var token_limit_count : u32 = 80;
+    var boundary_correct_count : u32 = 60;
+    var refusal_start_count : u32 = 40;
+    var push_failed_count : u32 = 30;
+    var unknown_count : u32 = 7;  // Should be < 10% of total
+    var crash_count : u32 = 0;
+    
+    var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                           refusal_start_count + push_failed_count + unknown_count + crash_count;
+    
+    // Verify unknown is less than 10% of total (success criterion from issue)
+    assert unknown_count < total_empty / 10;
+    
+    // Verify we can calculate breakdown percentages
+    var model_percentage : u32 = (model_ended_count * 100) / total_empty;
+    var token_percentage : u32 = (token_limit_count * 100) / total_empty;
+    var unknown_percentage : u32 = (unknown_count * 100) / total_empty;
+    
+    assert model_percentage > 0;
+    assert token_percentage > 0;
+    assert unknown_percentage < 10;  // Unknown should be < 10%
+    
+    // Verify no-diff vs crash categorization works
+    var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                              refusal_start_count + push_failed_count + unknown_count;
+    var crash_total : u32 = crash_count;
+    
+    assert no_diff_total == total_empty - crash_total;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/aging-a-label/card.png b/apps/website/public/ru/learn/aging-a-label/card.png new file mode 100644 index 0000000000..12562b123c Binary files /dev/null and b/apps/website/public/ru/learn/aging-a-label/card.png differ diff --git a/apps/website/public/ru/learn/aging-a-label/index.html b/apps/website/public/ru/learn/aging-a-label/index.html new file mode 100644 index 0000000000..3b5cf7fa3a --- /dev/null +++ b/apps/website/public/ru/learn/aging-a-label/index.html @@ -0,0 +1,238 @@ + + + + + + +Старение метки: курс «Королева и улей», урок 4 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Старение метки

+

Вы узнаете

Как задача с меткой поднимается с возрастом и почему только на один уровень.

+

Без меток Королева берёт задачи в том порядке, в котором их выдаёт GitHub. priority.t27 добавляет четыре уровня: critical, high, normal и low; меньший идёт раньше, а задача без метки считается normal. Задача с меткой поднимается на уровень через AGING_DAYS, 14 дней, и не больше чем на MAX_AGING_STEPS, 1 уровень, так что low доходит до normal на 14-й день и там остаётся. High и critical не стареют. Линию в виджете вычисляет effective_level.

+

Попробовать

Найдите день, когда задача с меткой low поднимается; затем снимите метку и дотяните до 60-го дня.

+

Открыть интерактивный урок →

+
Aging: how long a labelled low issue waits
Aging: how long a labelled low issue waits ↗
+

Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical.

+

specs/queen/priority.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366)
+; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no
+; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it.
+; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js`
+; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes.
+; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank,
+; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort):
+;   - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first);
+;   - an issue with an open blocker is skipped, not waited on (head-of-line blocking);
+;   - a cap on critical stops priority inflation; aging stops starvation of labelled low work;
+;   - an issue nobody labelled keeps today's place, so a repository with no labels sees no change.
+; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority
+; field (read it first once the org enables it; its four values map onto the same levels).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenPriority;
+
+/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages.
+pub const LEVEL_CRITICAL : u8 = 0;
+pub const LEVEL_HIGH : u8 = 1;
+pub const LEVEL_NORMAL : u8 = 2;
+pub const LEVEL_LOW : u8 = 3;
+
+/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing.
+/// When an issue carries labels of two levels, the more urgent one counts.
+pub const CRITICAL_LABELS : str = "P0,priority/critical,critical";
+pub const HIGH_LABELS : str = "P1,priority/high,high-priority";
+pub const NORMAL_LABELS : str = "P2,priority/medium";
+pub const LOW_LABELS : str = "P3,P4,priority/low";
+
+/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels,
+/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low"
+/// the fastest way to the front of a long queue.
+pub const AGING_DAYS : u32 = 14;
+pub const MAX_AGING_STEPS : u32 = 1;
+
+/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH.
+pub const CRITICAL_CAP : u32 = 3;
+
+/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs.
+pub const WHY_LISTING : u8 = 0;
+pub const WHY_LABEL : u8 = 1;
+pub const WHY_AGED : u8 = 2;
+pub const WHY_CAPPED : u8 = 3;
+pub const WHY_BLOCKED : u8 = 4;
+
+/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one.
+pub fn capped_level(base: u8, criticals_before: u32) -> u8 {
+    if (base == LEVEL_CRITICAL) {
+        if (criticals_before >= CRITICAL_CAP) {
+            return LEVEL_HIGH;
+        }
+    }
+    return base;
+}
+
+/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS)
+/// when it is labelled.
+pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 {
+    if (labelled == false) {
+        return level;
+    }
+    if (level <= LEVEL_HIGH) {
+        return level;
+    }
+    var steps : u32 = age_days / AGING_DAYS;
+    if (steps > MAX_AGING_STEPS) {
+        steps = MAX_AGING_STEPS;
+    }
+    var gap : u32 = (level as u32) - (LEVEL_HIGH as u32);
+    if (steps >= gap) {
+        return LEVEL_HIGH;
+    }
+    return ((level as u32) - steps) as u8;
+}
+
+/// An issue with an open blocker is not a candidate; the Queen moves past it.
+pub fn eligible(open_blockers: u32) -> bool {
+    return open_blockers == 0;
+}
+
+/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged
+/// issue never beats a real one of the same level), then earlier in GitHub's listing.
+pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool {
+    if (eff_a < eff_b) { return true; }
+    if (eff_a > eff_b) { return false; }
+    if (level_a < level_b) { return true; }
+    if (level_a > level_b) { return false; }
+    return index_a < index_b;
+}
+
+/// The reason printed for one issue.
+pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 {
+    if (open_blockers > 0) { return WHY_BLOCKED; }
+    if (level != base) { return WHY_CAPPED; }
+    if (eff != level) { return WHY_AGED; }
+    if (labelled) { return WHY_LABEL; }
+    return WHY_LISTING;
+}
+
+test no_label_keeps_the_listing_order {
+    assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1);
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false;
+    // #7077: a strict order, so a sort can use it: no issue outranks itself
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false;
+}
+
+test critical_runs_before_a_newer_unlabelled_issue {
+    assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0);
+}
+
+test unlabelled_never_beats_urgent {
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false;
+}
+
+test labelled_work_ages_one_level_and_never_to_critical {
+    assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW;
+    assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH;
+    assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH;
+}
+
+test an_aged_issue_loses_the_tie_to_a_real_one {
+    var aged : u8 = effective_level(LEVEL_LOW, true, 28);
+    assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0);
+    // #7077: of two LOW labels the aged one goes first, whatever the listing says
+    assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0);
+    assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false;
+}
+
+test the_fourth_critical_runs_as_high {
+    assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH;
+    assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW;
+}
+
+test a_blocked_issue_is_skipped {
+    assert eligible(0);
+    assert eligible(1) == false;
+    assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED;
+}
+
+test every_reason_is_reachable {
+    assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING;
+    assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL;
+    assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED;
+    assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED;
+}
+
+invariant levels_are_ordered {
+    assert LEVEL_CRITICAL < LEVEL_HIGH;
+    assert LEVEL_HIGH < LEVEL_NORMAL;
+    assert LEVEL_NORMAL < LEVEL_LOW;
+    assert CRITICAL_CAP > 0;
+    assert AGING_DAYS > 0;
+    assert MAX_AGING_STEPS == 1;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/back-off/card.png b/apps/website/public/ru/learn/back-off/card.png new file mode 100644 index 0000000000..6e8c1cea49 Binary files /dev/null and b/apps/website/public/ru/learn/back-off/card.png differ diff --git a/apps/website/public/ru/learn/back-off/index.html b/apps/website/public/ru/learn/back-off/index.html new file mode 100644 index 0000000000..1afcc100c7 --- /dev/null +++ b/apps/website/public/ru/learn/back-off/index.html @@ -0,0 +1,390 @@ + + + + + + +Отступить: курс «Королева и улей», урок 11 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Отступить

+

Вы узнаете

Почему строку, которая отвечает wait, проверяют всё реже и как часто хотя бы.

+

За 44 минуты на 2026-10-09 ревьюер сделал 941 визит к строкам, из них 888 с ответом wait, от 22 строк, чью ветку он не мог прочитать; одну строку проверили 54 раза. Теперь revisit_after_seconds ждёт 30 с после одного wait и удваивает паузу на каждый следующий, не больше 900 с: 30, 60, 120, 240, 480, 900. due_again даёт взять строку, только когда это время прошло, а любой другой ответ сбрасывает счёт.

+

Попробовать

Найдите число ожиданий, после которого пауза перестаёт расти; затем поставьте 4 ожидания и найдите секунду, когда строка снова к сроку.

+

Открыть интерактивный урок →

+
Back off: when the reviewer looks at a waiting row again
Back off: when the reviewer looks at a waiting row again ↗
+

A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due.

+

specs/queen/reviewer.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time
+; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the
+; live Queen (gHashTag/BrowserOS trios/agent-server):
+; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91);
+; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after
+;   another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to
+;   2 minutes -- in practice about one review a round;
+; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min).
+; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each
+; on its own lane, one row never twice at once, and every waiting row visited in turn. The round
+; then reviews nothing: two sweeps over one row would race on its verdict.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewer;
+
+/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy
+/// costs one query.
+pub const REVIEWER_EVERY_SECONDS : u32 = 10;
+
+/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the
+/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds).
+/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays
+/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour.
+pub const REVIEWER_CONCURRENCY : u32 = 4;
+
+/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and
+/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`).
+pub const REVIEWER_CEILING : u32 = 16;
+
+/// Seconds in an hour; finish rates are measured per hour.
+pub const SECONDS_PER_HOUR : u32 = 3600;
+
+/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer
+/// shares a 45 s budget with the round, because it no longer holds the round up.
+pub const REVIEW_ROW_SECONDS : u32 = 300;
+
+/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58
+/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS
+/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below
+/// the finish rate even if every review used its whole budget, and the backlog only grows.
+/// How many rows the reviewer may take at once, from what the host measures:
+/// - `keys` model keys, each with `lanes_per_key` lanes free for review;
+/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review);
+/// - never above REVIEWER_CEILING, never above the rows `waiting`;
+/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains.
+pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 {
+    if (waiting == 0) { return 0; }
+    var k : u32 = keys;
+    if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; }
+    var l : u32 = lanes_per_key;
+    if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; }
+    var bound : u32 = k * l;
+    if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; }
+    var by_memory : u32 = 1;
+    if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; }
+    if (by_memory < bound) { bound = by_memory; }
+    if (waiting < bound) { bound = waiting; }
+    if (bound < 1) { return 1; }
+    return bound;
+}
+
+/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every
+/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600).
+/// Split by the hour so no u32 product overflows.
+pub fn slots_needed(finished_per_hour: u32) -> u32 {
+    let whole : u32 = finished_per_hour / SECONDS_PER_HOUR;
+    let part : u32 = finished_per_hour % SECONDS_PER_HOUR;
+    let part_seconds : u32 = part * REVIEW_ROW_SECONDS;
+    var extra : u32 = part_seconds / SECONDS_PER_HOUR;
+    if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; }
+    return whole * REVIEW_ROW_SECONDS + extra;
+}
+
+/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows.
+pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool {
+    return slots >= slots_needed(finished_per_hour);
+}
+
+/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but
+/// no more than the rows waiting.
+pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 {
+    if (in_flight >= bound) { return 0; }
+    let room : u32 = bound - in_flight;
+    if (waiting < room) { return waiting; }
+    return room;
+}
+
+/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed
+/// REVIEWER_CONCURRENCY, unchanged for its existing callers.
+pub fn review_slots(in_flight: u32, waiting: u32) -> u32 {
+    return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting);
+}
+
+/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never
+/// visited before any it has. Without it, a row that is re-read and waits again (no review spent)
+/// keeps its place at the head and the reviewer circles the same few rows.
+pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool {
+    if (never_a and never_b == false) { return true; }
+    if (never_b and never_a == false) { return false; }
+    return seconds_since_a > seconds_since_b;
+}
+
+/// While the reviewer runs, the round reviews nothing.
+pub fn round_reviews(reviewer_running: bool) -> bool {
+    return reviewer_running == false;
+}
+
+/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen,
+/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could
+/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row
+/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned
+/// about 430 visits of the reviewer's slots.
+/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never
+/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that
+/// has started to read is visited at the normal pace again.
+pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30;
+pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900;
+
+pub fn revisit_after_seconds(waits: u32) -> u32 {
+    if (waits == 0) { return 0; }
+    var wait : u32 = WAIT_REVISIT_BASE_SECONDS;
+    var n : u32 = 1;
+    while (n < waits) {
+        if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+    return wait;
+}
+
+/// Whether a row may be taken now: one that never waited always may, and one that waited only after
+/// its revisit time has passed since the last visit.
+pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool {
+    return seconds_since_visit >= revisit_after_seconds(waits);
+}
+
+/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the
+/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait`
+/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only
+/// when the bee committed something, so a bee that committed nothing leaves no branch here, the
+/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed.
+/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free
+/// again after the retry floor.
+/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle
+/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit,
+/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty
+/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits,
+/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner`
+/// false) and a row whose bundle is stored are never decided here: their diff failed for some
+/// other reason, and that still decides nothing.
+pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10;
+
+pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool {
+    if (on_runner == false) { return false; }
+    if (bundle_stored) { return false; }
+    return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES;
+}
+
+test a_pass_fills_the_free_slots_with_waiting_rows {
+    assert review_slots(0, 0) == 0;
+    assert review_slots(0, 2) == 2;
+    assert review_slots(0, 100) == REVIEWER_CONCURRENCY;
+    assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1;
+    assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1;
+    assert review_slots(REVIEWER_CONCURRENCY, 100) == 0;
+    assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0;
+}
+
+test the_bound_follows_lanes_memory_and_backlog {
+    // nothing waits: no slot, whatever the host has
+    assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0;
+    // lanes bound: 2 keys x 3 lanes
+    assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6;
+    // memory bound: 1000 MB at 300 MB a review
+    assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3;
+    // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free
+    assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16;
+    assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING;
+    // never above the backlog
+    assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2;
+    // at least one while anything waits, even with no lane or memory measured
+    assert reviewer_concurrency(0, 0, 0, 0, 1) == 1;
+    assert reviewer_concurrency(10, 10, 100, 300, 111) == 1;
+    assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1;
+    assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1;
+}
+
+test the_measured_load_needs_five_and_the_bound_admits_them {
+    // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5
+    assert slots_needed(58) == 5;
+    assert slots_needed(0) == 0;
+    assert slots_needed(12) == 1;
+    assert slots_needed(13) == 2;
+    assert slots_needed(3600) == REVIEW_ROW_SECONDS;
+    assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1;
+    assert slots_needed(4294967295) == 357913942;
+    // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true;
+    assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16;
+    assert keeps_up(4, 48) == true;
+    assert keeps_up(4, 49) == false;
+}
+
+test a_pass_fills_the_free_slots_against_the_computed_bound {
+    let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111);
+    assert review_slots_within(bound, 0, 111) == 16;
+    assert review_slots_within(bound, 11, 111) == 5;
+    assert review_slots_within(bound, 15, 3) == 1;
+    assert review_slots_within(bound, 16, 111) == 0;
+    assert review_slots_within(bound, 17, 111) == 0;
+    assert review_slots_within(bound, 0, 3) == 3;
+    assert review_slots_within(0, 0, 111) == 0;
+}
+
+test every_row_is_visited_in_turn {
+    // never visited goes first
+    assert visit_first(true, 0, false, 9999) == true;
+    assert visit_first(false, 9999, true, 0) == false;
+    // otherwise the one visited longest ago
+    assert visit_first(false, 300, false, 20) == true;
+    assert visit_first(false, 20, false, 300) == false;
+    assert visit_first(false, 20, false, 20) == false;
+    assert visit_first(true, 0, true, 0) == false;
+}
+
+test the_round_stands_aside_for_the_reviewer {
+    assert round_reviews(true) == false;
+    assert round_reviews(false) == true;
+}
+
+test a_row_that_keeps_waiting_is_visited_less_and_less {
+    assert revisit_after_seconds(0) == 0;
+    assert revisit_after_seconds(1) == 30;
+    assert revisit_after_seconds(2) == 60;
+    assert revisit_after_seconds(3) == 120;
+    assert revisit_after_seconds(5) == 480;
+    assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS;
+    // a row that never waited is always due; one that waited is due only once its time has passed
+    assert due_again(0, 0) == true;
+    assert due_again(1, 29) == false;
+    assert due_again(1, 30) == true;
+    assert due_again(6, 899) == false;
+    assert due_again(6, 900) == true;
+}
+
+test no_bundle_is_empty_for_a_runner_row_past_the_grace {
+    // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true;
+    // the six hours the frozen-wait valve would otherwise have held it
+    assert no_bundle_is_empty(true, false, 360) == true;
+    assert no_bundle_is_empty(true, false, 4294967295) == true;
+}
+
+test no_bundle_is_empty_waits_inside_the_grace {
+    // just finished: the runner may not have stored its bundle yet
+    assert no_bundle_is_empty(true, false, 0) == false;
+    // a bundle 40 s late is still awaited, and judged on its branch when it arrives
+    assert no_bundle_is_empty(true, false, 1) == false;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false;
+}
+
+test no_bundle_is_empty_leaves_a_container_row_alone {
+    // a bee that ran here has no bundle by design; its diff failing decides nothing
+    assert no_bundle_is_empty(false, false, 0) == false;
+    assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(false, false, 4294967295) == false;
+    assert no_bundle_is_empty(false, true, 4294967295) == false;
+}
+
+test no_bundle_is_empty_leaves_a_stored_bundle_alone {
+    // the bundle is there, so the diff failed for another reason: still a wait
+    assert no_bundle_is_empty(true, true, 0) == false;
+    assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(true, true, 4294967295) == false;
+}
+
+invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour {
+    // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit
+    assert NO_BUNDLE_GRACE_MINUTES > 1;
+    assert no_bundle_is_empty(true, false, 1) == false;
+    // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit
+    // that judges it comes within the hour of its finish, not after the six-hour valve
+    assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR;
+}
+
+invariant the_reviewer_is_quicker_than_the_round {
+    assert REVIEWER_EVERY_SECONDS < 60;
+    assert REVIEWER_CONCURRENCY > 1;
+    assert REVIEW_ROW_SECONDS >= 120;
+    // a stuck row is still seen several times an hour, so one that starts to read is not lost for long
+    assert WAIT_REVISIT_CAP_SECONDS <= 3600;
+    assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS;
+}
+
+invariant the_reviewer_can_outrun_the_measured_finish_rate {
+    // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets
+    // at least the slots that load needs, and the ceiling is not what stops it
+    assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58);
+    assert REVIEWER_CEILING >= slots_needed(58);
+    assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY;
+    // the fixed bound alone could not, which is why it is only the fallback
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours
+    assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0;
+    // FR-001: 0 only when nothing waits
+    assert reviewer_concurrency(0, 0, 0, 0, 0) == 0;
+    assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/enough-reviewers/card.png b/apps/website/public/ru/learn/enough-reviewers/card.png new file mode 100644 index 0000000000..5ba13e1d84 Binary files /dev/null and b/apps/website/public/ru/learn/enough-reviewers/card.png differ diff --git a/apps/website/public/ru/learn/enough-reviewers/index.html b/apps/website/public/ru/learn/enough-reviewers/index.html new file mode 100644 index 0000000000..460a6ad890 --- /dev/null +++ b/apps/website/public/ru/learn/enough-reviewers/index.html @@ -0,0 +1,390 @@ + + + + + + +Хватит ли ревьюеров: курс «Королева и улей», урок 10 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Хватит ли ревьюеров

+

Вы узнаете

Сколько ревью должно идти сразу, чтобы ревьюер успевал за пчёлами.

+

Ревью может занять REVIEW_ROW_SECONDS, 300 с, поэтому один слот проверяет не больше 12 строк в час. На 2026-10-09 пчёлы заканчивали около 58 в час, пока ждали 111 строк, а постоянный REVIEWER_CONCURRENCY, равный 4, даёт не больше 48. slots_needed(58) равно 5. reviewer_concurrency учитывает ключи, их полосы и свободную память, никогда не поднимается выше 16 и не опускается ниже 1, пока что-то ждёт, а keeps_up сравнивает это с темпом завершения.

+

Попробовать

Найдите наименьшее число ключей с 1 полосой, при котором ревьюер успевает за 58 в час; затем урежьте память до 1000 МБ.

+

Открыть интерактивный урок →

+
How many reviewer bees: keeping up with the finish rate
How many reviewer bees: keeping up with the finish rate ↗
+

Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work.

+

specs/queen/reviewer.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time
+; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the
+; live Queen (gHashTag/BrowserOS trios/agent-server):
+; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91);
+; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after
+;   another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to
+;   2 minutes -- in practice about one review a round;
+; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min).
+; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each
+; on its own lane, one row never twice at once, and every waiting row visited in turn. The round
+; then reviews nothing: two sweeps over one row would race on its verdict.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewer;
+
+/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy
+/// costs one query.
+pub const REVIEWER_EVERY_SECONDS : u32 = 10;
+
+/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the
+/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds).
+/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays
+/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour.
+pub const REVIEWER_CONCURRENCY : u32 = 4;
+
+/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and
+/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`).
+pub const REVIEWER_CEILING : u32 = 16;
+
+/// Seconds in an hour; finish rates are measured per hour.
+pub const SECONDS_PER_HOUR : u32 = 3600;
+
+/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer
+/// shares a 45 s budget with the round, because it no longer holds the round up.
+pub const REVIEW_ROW_SECONDS : u32 = 300;
+
+/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58
+/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS
+/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below
+/// the finish rate even if every review used its whole budget, and the backlog only grows.
+/// How many rows the reviewer may take at once, from what the host measures:
+/// - `keys` model keys, each with `lanes_per_key` lanes free for review;
+/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review);
+/// - never above REVIEWER_CEILING, never above the rows `waiting`;
+/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains.
+pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 {
+    if (waiting == 0) { return 0; }
+    var k : u32 = keys;
+    if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; }
+    var l : u32 = lanes_per_key;
+    if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; }
+    var bound : u32 = k * l;
+    if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; }
+    var by_memory : u32 = 1;
+    if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; }
+    if (by_memory < bound) { bound = by_memory; }
+    if (waiting < bound) { bound = waiting; }
+    if (bound < 1) { return 1; }
+    return bound;
+}
+
+/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every
+/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600).
+/// Split by the hour so no u32 product overflows.
+pub fn slots_needed(finished_per_hour: u32) -> u32 {
+    let whole : u32 = finished_per_hour / SECONDS_PER_HOUR;
+    let part : u32 = finished_per_hour % SECONDS_PER_HOUR;
+    let part_seconds : u32 = part * REVIEW_ROW_SECONDS;
+    var extra : u32 = part_seconds / SECONDS_PER_HOUR;
+    if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; }
+    return whole * REVIEW_ROW_SECONDS + extra;
+}
+
+/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows.
+pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool {
+    return slots >= slots_needed(finished_per_hour);
+}
+
+/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but
+/// no more than the rows waiting.
+pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 {
+    if (in_flight >= bound) { return 0; }
+    let room : u32 = bound - in_flight;
+    if (waiting < room) { return waiting; }
+    return room;
+}
+
+/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed
+/// REVIEWER_CONCURRENCY, unchanged for its existing callers.
+pub fn review_slots(in_flight: u32, waiting: u32) -> u32 {
+    return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting);
+}
+
+/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never
+/// visited before any it has. Without it, a row that is re-read and waits again (no review spent)
+/// keeps its place at the head and the reviewer circles the same few rows.
+pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool {
+    if (never_a and never_b == false) { return true; }
+    if (never_b and never_a == false) { return false; }
+    return seconds_since_a > seconds_since_b;
+}
+
+/// While the reviewer runs, the round reviews nothing.
+pub fn round_reviews(reviewer_running: bool) -> bool {
+    return reviewer_running == false;
+}
+
+/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen,
+/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could
+/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row
+/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned
+/// about 430 visits of the reviewer's slots.
+/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never
+/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that
+/// has started to read is visited at the normal pace again.
+pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30;
+pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900;
+
+pub fn revisit_after_seconds(waits: u32) -> u32 {
+    if (waits == 0) { return 0; }
+    var wait : u32 = WAIT_REVISIT_BASE_SECONDS;
+    var n : u32 = 1;
+    while (n < waits) {
+        if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; }
+    return wait;
+}
+
+/// Whether a row may be taken now: one that never waited always may, and one that waited only after
+/// its revisit time has passed since the last visit.
+pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool {
+    return seconds_since_visit >= revisit_after_seconds(waits);
+}
+
+/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the
+/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait`
+/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only
+/// when the bee committed something, so a bee that committed nothing leaves no branch here, the
+/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed.
+/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free
+/// again after the retry floor.
+/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle
+/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit,
+/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty
+/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits,
+/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner`
+/// false) and a row whose bundle is stored are never decided here: their diff failed for some
+/// other reason, and that still decides nothing.
+pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10;
+
+pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool {
+    if (on_runner == false) { return false; }
+    if (bundle_stored) { return false; }
+    return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES;
+}
+
+test a_pass_fills_the_free_slots_with_waiting_rows {
+    assert review_slots(0, 0) == 0;
+    assert review_slots(0, 2) == 2;
+    assert review_slots(0, 100) == REVIEWER_CONCURRENCY;
+    assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1;
+    assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1;
+    assert review_slots(REVIEWER_CONCURRENCY, 100) == 0;
+    assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0;
+}
+
+test the_bound_follows_lanes_memory_and_backlog {
+    // nothing waits: no slot, whatever the host has
+    assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0;
+    // lanes bound: 2 keys x 3 lanes
+    assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6;
+    // memory bound: 1000 MB at 300 MB a review
+    assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3;
+    // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free
+    assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16;
+    assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING;
+    // never above the backlog
+    assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2;
+    // at least one while anything waits, even with no lane or memory measured
+    assert reviewer_concurrency(0, 0, 0, 0, 1) == 1;
+    assert reviewer_concurrency(10, 10, 100, 300, 111) == 1;
+    assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1;
+    assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1;
+}
+
+test the_measured_load_needs_five_and_the_bound_admits_them {
+    // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5
+    assert slots_needed(58) == 5;
+    assert slots_needed(0) == 0;
+    assert slots_needed(12) == 1;
+    assert slots_needed(13) == 2;
+    assert slots_needed(3600) == REVIEW_ROW_SECONDS;
+    assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1;
+    assert slots_needed(4294967295) == 357913942;
+    // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true;
+    assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16;
+    assert keeps_up(4, 48) == true;
+    assert keeps_up(4, 49) == false;
+}
+
+test a_pass_fills_the_free_slots_against_the_computed_bound {
+    let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111);
+    assert review_slots_within(bound, 0, 111) == 16;
+    assert review_slots_within(bound, 11, 111) == 5;
+    assert review_slots_within(bound, 15, 3) == 1;
+    assert review_slots_within(bound, 16, 111) == 0;
+    assert review_slots_within(bound, 17, 111) == 0;
+    assert review_slots_within(bound, 0, 3) == 3;
+    assert review_slots_within(0, 0, 111) == 0;
+}
+
+test every_row_is_visited_in_turn {
+    // never visited goes first
+    assert visit_first(true, 0, false, 9999) == true;
+    assert visit_first(false, 9999, true, 0) == false;
+    // otherwise the one visited longest ago
+    assert visit_first(false, 300, false, 20) == true;
+    assert visit_first(false, 20, false, 300) == false;
+    assert visit_first(false, 20, false, 20) == false;
+    assert visit_first(true, 0, true, 0) == false;
+}
+
+test the_round_stands_aside_for_the_reviewer {
+    assert round_reviews(true) == false;
+    assert round_reviews(false) == true;
+}
+
+test a_row_that_keeps_waiting_is_visited_less_and_less {
+    assert revisit_after_seconds(0) == 0;
+    assert revisit_after_seconds(1) == 30;
+    assert revisit_after_seconds(2) == 60;
+    assert revisit_after_seconds(3) == 120;
+    assert revisit_after_seconds(5) == 480;
+    assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS;
+    assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS;
+    // a row that never waited is always due; one that waited is due only once its time has passed
+    assert due_again(0, 0) == true;
+    assert due_again(1, 29) == false;
+    assert due_again(1, 30) == true;
+    assert due_again(6, 899) == false;
+    assert due_again(6, 900) == true;
+}
+
+test no_bundle_is_empty_for_a_runner_row_past_the_grace {
+    // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true;
+    // the six hours the frozen-wait valve would otherwise have held it
+    assert no_bundle_is_empty(true, false, 360) == true;
+    assert no_bundle_is_empty(true, false, 4294967295) == true;
+}
+
+test no_bundle_is_empty_waits_inside_the_grace {
+    // just finished: the runner may not have stored its bundle yet
+    assert no_bundle_is_empty(true, false, 0) == false;
+    // a bundle 40 s late is still awaited, and judged on its branch when it arrives
+    assert no_bundle_is_empty(true, false, 1) == false;
+    assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false;
+}
+
+test no_bundle_is_empty_leaves_a_container_row_alone {
+    // a bee that ran here has no bundle by design; its diff failing decides nothing
+    assert no_bundle_is_empty(false, false, 0) == false;
+    assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(false, false, 4294967295) == false;
+    assert no_bundle_is_empty(false, true, 4294967295) == false;
+}
+
+test no_bundle_is_empty_leaves_a_stored_bundle_alone {
+    // the bundle is there, so the diff failed for another reason: still a wait
+    assert no_bundle_is_empty(true, true, 0) == false;
+    assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false;
+    assert no_bundle_is_empty(true, true, 4294967295) == false;
+}
+
+invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour {
+    // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit
+    assert NO_BUNDLE_GRACE_MINUTES > 1;
+    assert no_bundle_is_empty(true, false, 1) == false;
+    // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit
+    // that judges it comes within the hour of its finish, not after the six-hour valve
+    assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR;
+}
+
+invariant the_reviewer_is_quicker_than_the_round {
+    assert REVIEWER_EVERY_SECONDS < 60;
+    assert REVIEWER_CONCURRENCY > 1;
+    assert REVIEW_ROW_SECONDS >= 120;
+    // a stuck row is still seen several times an hour, so one that starts to read is not lost for long
+    assert WAIT_REVISIT_CAP_SECONDS <= 3600;
+    assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS;
+}
+
+invariant the_reviewer_can_outrun_the_measured_finish_rate {
+    // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets
+    // at least the slots that load needs, and the ceiling is not what stops it
+    assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58);
+    assert REVIEWER_CEILING >= slots_needed(58);
+    assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY;
+    // the fixed bound alone could not, which is why it is only the fallback
+    assert keeps_up(REVIEWER_CONCURRENCY, 58) == false;
+    // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours
+    assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0;
+    // FR-001: 0 only when nothing waits
+    assert reviewer_concurrency(0, 0, 0, 0, 0) == 0;
+    assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/index.html b/apps/website/public/ru/learn/index.html index 8ad8d74417..41e31f78ab 100644 --- a/apps/website/public/ru/learn/index.html +++ b/apps/website/public/ru/learn/index.html @@ -63,7 +63,7 @@

Основы t27: язык за 27 уроков

Перед курсом по FPGA: что такое спека t27, как компилятор её читает и что отдаёт в ответ. Каждый урок открывает таблицу или схему из своей спеки и одну спеку, которую можно запустить в браузере.

Курс 0, 27 уроков, в каждом инфографика и спека t27: модуль, константы и типы, функции и тесты, затем стадии компилятора и 7 языков, которые он пишет. Идёт перед курсом по FPGA.

Открыть интерактивный курс →

-
  1. Основы t27: язык за 27 уроков
  2. С нуля до чипа: программирование FPGA на t27
  3. ИИ-числа на t27: от форматов чисел до тернарной сети
  4. GoldenFloat на t27: одно правило, семнадцать ширин чисел
  5. Проверка железа с t27: тестбенчи, временные диаграммы, формальные методы, мутации
  6. Тактирование, сброс и пересечения тактовых доменов на t27
  7. Шины и периферия на t27: UART, SPI, APB, AXI4, Ethernet
  8. Троичная машина: от трита до работающей программы
  9. Арифметика и DSP на FPGA: от цепи переноса до фильтра
  10. Нейросети на t27: от одного нейрона до блока трансформера
+
  1. Основы t27: язык за 27 уроков
  2. С нуля до чипа: программирование FPGA на t27
  3. ИИ-числа на t27: от форматов чисел до тернарной сети
  4. GoldenFloat на t27: одно правило, семнадцать ширин чисел
  5. Проверка железа с t27: тестбенчи, временные диаграммы, формальные методы, мутации
  6. Тактирование, сброс и пересечения тактовых доменов на t27
  7. Шины и периферия на t27: UART, SPI, APB, AXI4, Ethernet
  8. Троичная машина: от трита до работающей программы
  9. Арифметика и DSP на FPGA: от цепи переноса до фильтра
  10. Нейросети на t27: от одного нейрона до блока трансформера
  11. Королева и улей: как проект t27 строит себя сам

Все уроки

diff --git a/apps/website/public/ru/learn/inherited-red/card.png b/apps/website/public/ru/learn/inherited-red/card.png new file mode 100644 index 0000000000..3ede31beba Binary files /dev/null and b/apps/website/public/ru/learn/inherited-red/card.png differ diff --git a/apps/website/public/ru/learn/inherited-red/index.html b/apps/website/public/ru/learn/inherited-red/index.html new file mode 100644 index 0000000000..1dd4349b57 --- /dev/null +++ b/apps/website/public/ru/learn/inherited-red/index.html @@ -0,0 +1,377 @@ + + + + + + +Унаследованный красный: курс «Королева и улей», урок 15 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Унаследованный красный

+

Вы узнаете

Когда красная проверка пулл-реквеста унаследована от master и почему отменённый запуск ничего не говорит.

+

Пчела может одобрить PR с красной проверкой, если эта проверка красная и в master. На 2026-10-06 PR слили красным, потому что запуск этой проверки в master был отменён и прочитан как красный. Теперь мерджер сам читает master, начиная с самого нового запуска: run_says учитывает только завершённое падение или успех, look_back пропускает всё остальное и запуски старше базы слияния, а discount_holds нужны слово пчелы и MASTER RED.

+

Попробовать

Сделайте все запуски отменёнными; затем передвиньте падение раньше базы слияния и посмотрите на ворота.

+

Открыть интерактивный урок →

+
Inherited red: what master's runs say about a red check
Inherited red: what master's runs say about a red check ↗
+

The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts.

+

specs/queen/merger_gate.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved
+; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq).
+; It does not import this spec, and t27c gen-js lowers no function bodies (#7300).
+; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario
+; asserts the verdict that scenario pins for the workflow.
+; Negative controls: discounted required check, unconcluded check, required check never posted,
+; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run
+; of the same check was CANCELLED (#7317)
+; ASCII only (L3)
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenMergerGate;
+
+/// With the ruleset unreadable the merger cannot tell which checks are required,
+/// so every check counts as required and no red one is discountable (#5776 item 2).
+pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool {
+    return !ruleset_readable || required;
+}
+
+/// A red check passes only when it is not required, has concluded, and is discounted
+pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool {
+    return !required && concluded && discounted;
+}
+
+/// A required check passes when it is posted and green
+pub fn required_check_passes(posted: bool, green: bool) -> bool {
+    return posted && green;
+}
+
+/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks;
+/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check
+/// that does not count as required.
+pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool {
+    if (!concluded) { return false; }
+    if (green) { return true; }
+    return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted);
+}
+
+// ---------------------------------------------------------------------------
+// What master says about a discounted red (#7317)
+//
+// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its
+// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled
+// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it.
+// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the
+// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or
+// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts.
+
+/// One master run of the red check, as the merger reads its job's status and conclusion.
+/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code.
+pub const RUN_MISSING : u8 = 0;
+pub const RUN_FAILURE : u8 = 1;
+pub const RUN_SUCCESS : u8 = 2;
+pub const RUN_CANCELLED : u8 = 3;
+pub const RUN_SKIPPED : u8 = 4;
+pub const RUN_TIMED_OUT : u8 = 5;
+/// queued or in progress
+pub const RUN_RUNNING : u8 = 6;
+/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail
+pub const RUN_OTHER : u8 = 7;
+
+/// What master says about the PR's red.
+pub const MASTER_UNKNOWN : u8 = 0;
+/// a completed failure: the red is inherited
+pub const MASTER_RED : u8 = 1;
+/// a completed success: master was green, the red is the PR's own
+pub const MASTER_GREEN : u8 = 2;
+
+/// One run on its own. Only a completed failure or a completed success decides anything.
+pub fn run_says(run: u8) -> u8 {
+    if (run == RUN_FAILURE) { return MASTER_RED; }
+    if (run == RUN_SUCCESS) { return MASTER_GREEN; }
+    return MASTER_UNKNOWN;
+}
+
+/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN.
+/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not.
+/// A run older than the PR's merge base is not evidence about the code the PR started from.
+pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 {
+    if (so_far != MASTER_UNKNOWN) { return so_far; }
+    if (!at_or_after_merge_base) { return MASTER_UNKNOWN; }
+    return run_says(run);
+}
+
+/// A red is discounted only when the bee's approval names it AND master says red.
+/// This is the `discounted` that check_passes takes.
+pub fn discount_holds(bee_named: bool, master: u8) -> bool {
+    return bee_named && master == MASTER_RED;
+}
+
+/// The gate opens when at least one check has posted, none is still running,
+/// and none blocks. Zero checks is not zero failures.
+pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool {
+    return posted > 0 && running == 0 && blocking == 0;
+}
+
+// Tests for counts_as_required
+
+test counts_as_required_follows_the_ruleset_when_readable {
+    assert counts_as_required(true, true) == true;
+    assert counts_as_required(true, false) == false;
+}
+
+test counts_as_required_is_every_check_when_unreadable {
+    assert counts_as_required(false, false) == true;
+    assert counts_as_required(false, true) == true;
+}
+
+// Tests for red_check_passes
+
+// Positive case: non-required check that concluded and is discounted
+test red_check_passes_valid_discounted_non_required {
+    assert red_check_passes(false, true, true) == true;
+}
+
+// Negative control: discounted required check (required=true, cannot be discounted)
+test red_check_passes_discounted_required_fails {
+    assert red_check_passes(true, true, true) == false;
+}
+
+// Negative control: unconcluded check (concluded=false, even if discounted)
+test red_check_passes_unconcluded_fails {
+    assert red_check_passes(false, false, true) == false;
+}
+
+// Negative control: non-required check that concluded but not discounted
+test red_check_passes_non_discounted_fails {
+    assert red_check_passes(false, true, false) == false;
+}
+
+test red_check_passes_all_false_fails {
+    assert red_check_passes(true, false, false) == false;
+}
+
+// Tests for required_check_passes
+
+test required_check_passes_posted_and_green_passes {
+    assert required_check_passes(true, true) == true;
+}
+
+// Self-test scenario "required check never posted"
+test required_check_passes_not_posted_fails {
+    assert required_check_passes(false, true) == false;
+}
+
+test required_check_passes_posted_but_not_green_fails {
+    assert required_check_passes(true, false) == false;
+}
+
+test required_check_passes_neither_posted_nor_green_fails {
+    assert required_check_passes(false, false) == false;
+}
+
+// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted)
+
+test check_passes_green_advisory_and_green_required_pass {
+    assert check_passes(true, false, true, true, false) == true;
+    assert check_passes(true, true, true, true, false) == true;
+}
+
+// Self-test scenario "red advisory check, discounted in the bee's approval"
+test check_passes_red_advisory_discounted_passes {
+    assert check_passes(true, false, true, false, true) == true;
+}
+
+// Self-test scenario "red advisory check, not discounted"
+test check_passes_red_advisory_not_discounted_blocks {
+    assert check_passes(true, false, true, false, false) == false;
+}
+
+// Self-test scenario "red REQUIRED check, discounted anyway"
+test check_passes_red_required_discounted_blocks {
+    assert check_passes(true, true, true, false, true) == false;
+}
+
+// Self-test scenarios "a check still running" and "pending commit status blocks":
+// not concluded blocks whatever else is true, a discount included
+test check_passes_running_check_blocks {
+    assert check_passes(true, false, false, true, true) == false;
+    assert check_passes(true, false, false, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable: a discounted red check still blocks"
+test check_passes_unreadable_ruleset_discounted_red_blocks {
+    assert check_passes(false, false, true, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable, every check green"
+test check_passes_unreadable_ruleset_green_passes {
+    assert check_passes(false, false, true, true, false) == true;
+}
+
+// Tests for gate_open: (posted, running, blocking)
+
+// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks
+test gate_open_posted_checks_all_pass_opens {
+    assert gate_open(2, 0, 0) == true;
+    assert gate_open(1, 0, 0) == true;
+}
+
+// Negative control: zero checks is not zero failures
+test gate_open_zero_checks_stays_closed {
+    assert gate_open(0, 0, 0) == false;
+}
+
+// Self-test scenario "a check still running"
+test gate_open_running_check_keeps_it_closed {
+    assert gate_open(3, 1, 0) == false;
+}
+
+// Self-test scenario "two red checks, only one discounted"
+test gate_open_one_blocking_check_keeps_it_closed {
+    assert gate_open(4, 0, 1) == false;
+}
+
+// Tests for run_says, look_back and discount_holds (#7317)
+
+test only_a_completed_failure_says_red {
+    assert run_says(RUN_FAILURE) == MASTER_RED;
+    assert run_says(RUN_SUCCESS) == MASTER_GREEN;
+}
+
+test every_other_run_says_unknown {
+    assert run_says(RUN_MISSING) == MASTER_UNKNOWN;
+    assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN;
+    assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN;
+    assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN;
+    assert run_says(RUN_RUNNING) == MASTER_UNKNOWN;
+    assert run_says(RUN_OTHER) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master newest cancelled, older failure at or after the merge base"
+test look_back_passes_a_cancelled_run_to_an_older_failure {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED;
+}
+
+// Self-test scenario "master failure only before the merge base"
+test look_back_stops_at_the_merge_base {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN;
+    assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN;
+}
+
+// The newest decisive run wins, in both directions
+test look_back_keeps_the_newest_decisive_run {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN;
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED;
+    assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED;
+}
+
+test look_back_over_only_unknown_runs_stays_unknown {
+    assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master failure": discounted
+test discount_holds_on_a_master_failure {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true;
+}
+
+// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked
+test pr_red_while_master_cancelled_is_blocked {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false;
+}
+
+// Self-test scenario "master success": the red is the PR's own -> blocked
+test pr_red_while_master_green_is_blocked {
+    assert discount_holds(true, MASTER_GREEN) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false;
+}
+
+// Master red, but the bee never named the check: blocked
+test master_red_without_the_bees_line_is_blocked {
+    assert discount_holds(false, MASTER_RED) == false;
+    assert discount_holds(false, MASTER_UNKNOWN) == false;
+}
+
+invariant the_run_and_master_codes_never_move {
+    assert RUN_MISSING == 0;
+    assert RUN_FAILURE == 1;
+    assert RUN_SUCCESS == 2;
+    assert RUN_CANCELLED == 3;
+    assert RUN_SKIPPED == 4;
+    assert RUN_TIMED_OUT == 5;
+    assert RUN_RUNNING == 6;
+    assert RUN_OTHER == 7;
+    assert MASTER_UNKNOWN == 0;
+    assert MASTER_RED == 1;
+    assert MASTER_GREEN == 2;
+}
+
+invariant unknown_never_discounts {
+    assert discount_holds(true, MASTER_UNKNOWN) == false;
+    assert discount_holds(true, run_says(RUN_MISSING)) == false;
+    assert discount_holds(true, run_says(RUN_CANCELLED)) == false;
+    assert discount_holds(true, run_says(RUN_SKIPPED)) == false;
+    assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false;
+    assert discount_holds(true, run_says(RUN_RUNNING)) == false;
+    assert discount_holds(true, run_says(RUN_OTHER)) == false;
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/neural-networks/index.html b/apps/website/public/ru/learn/neural-networks/index.html index cf00d6c5ae..52c0e9fd4d 100644 --- a/apps/website/public/ru/learn/neural-networks/index.html +++ b/apps/website/public/ru/learn/neural-networks/index.html @@ -63,7 +63,7 @@

Нейросети на t27: от одного нейрона до блок

Агенты этого проекта думают нейросетями, а здесь их части записаны спеками t27 с тестами, которые проверяет компилятор. Каждый виджет исполняет тела fn спеки из specs/ml, скомпилированные в WebAssembly: двигаете ползунок — отвечает спека.

Курс 10, 27 уроков, в каждом живой виджет и спека t27: функции активации, softmax и эмбеддинги, пулинг и пакетная нормализация, функции потерь, SGD, RMSProp и Adam, обратное распространение и XOR, рекуррентные ячейки, внимание, блок трансформера и PPO. Каждое число вычисляет скомпилированная спека.

Открыть интерактивный курс →

- +

Все уроки

diff --git a/apps/website/public/ru/learn/no-credit-for-keys/card.png b/apps/website/public/ru/learn/no-credit-for-keys/card.png new file mode 100644 index 0000000000..8f70346800 Binary files /dev/null and b/apps/website/public/ru/learn/no-credit-for-keys/card.png differ diff --git a/apps/website/public/ru/learn/no-credit-for-keys/index.html b/apps/website/public/ru/learn/no-credit-for-keys/index.html new file mode 100644 index 0000000000..7a5d65019d --- /dev/null +++ b/apps/website/public/ru/learn/no-credit-for-keys/index.html @@ -0,0 +1,258 @@ + + + + + + +Ключи не дают заслуг: курс «Королева и улей», урок 19 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Ключи не дают заслуг

+

Вы узнаете

Что приносят ключи, которые одолжил участник, и какие пределы их берегут.

+

Любой может одолжить улью ключи моделей через queen-contributor-keys.t27. Ключ проверяют, прежде чем включить: не больше 4 проверок сразу, новая проверка ждёт 60 с после прошлой, а у одного владельца не больше 100 ключей. Сам по себе ключ ничего не приносит: CREDIT_WALLET ложно, поэтому wallet_credit отвечает 0 при любом числе ключей. Опыт следует за работой, которую записывает Королева.

+

Попробовать

Протяните число ключей от 0 до 100 и посмотрите на счётчик заслуг; затем найдите секунду, когда можно начать проверку.

+

Открыть интерактивный урок →

+
Lending a key earns no credit: XP follows recorded work
Lending a key earns no credit: XP follows recorded work ↗
+

A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s.

+

specs/automation/queen-contributor-keys.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/queen-contributor-keys.t27 -- private contributor key lifecycle.
+// Host: BrowserOS trios/agent-server/apps/server/src/api/services/queen-contributor-keys.ts.
+// Trace: gHashTag/t27#5472; gHashTag/999-multibots-telegraf#3251.
+// Generated: t27c gen-ts supplies the host policy constants.
+// Claim status: executable contract; PostgreSQL and live provider evidence are separate.
+// XP follows work recorded by the Queen. Adding credentials never credits a wallet.
+// Negative managed indices and nonnegative environment indices share one history.
+// v1 (2026-10-01): trusted proxy, persistent ownership and bounded provider probes.
+// v2 (2026-10-02): one model for all of an owner's keys of a provider, accepted only
+// after a key of that provider called a tool with it. Probe timeout 15 s -> 90 s:
+// z-ai/glm-5.3 on NVIDIA answered in 20-46 s and glm-4.5-flash on Z.ai in up to 87 s.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::queen_contributor_keys {
+    pub const KIND : str = "automation";
+    pub const ID : str = "queen-contributor-keys";
+    pub const REPO : str = "BrowserOS";
+    pub const VERSION : u8 = 2;
+
+    pub const PROBE_TIMEOUT_MS : u32 = 90000;
+    pub const PROBE_MAX_TOKENS : u16 = 16;
+    pub const MAX_PROBES_IN_FLIGHT : u16 = 4;
+    pub const PROBE_COOLDOWN_SECONDS : u16 = 60;
+    pub const MAX_KEYS_PER_OWNER : u16 = 100;
+    pub const MAX_KEY_BYTES : u16 = 4096;
+    pub const MAX_BODY_BYTES : u16 = 8192;
+    pub const LABEL_LIMIT : u16 = 80;
+    pub const PROXY_TOKEN_MIN_BYTES : u16 = 32;
+    pub const OWNER_REASSIGNMENT : bool = false;
+    pub const CREDIT_WALLET : bool = false;
+    pub const ENVIRONMENT_INDICES_COMPACTED : bool = false;
+    pub const NVIDIA_URL : str = "https://integrate.api.nvidia.com/v1";
+    pub const NVIDIA_MODEL : str = "nvidia/nemotron-3-ultra-550b-a55b";
+    pub const ZAI_URL : str = "https://api.z.ai/api/paas/v4";
+    pub const ZAI_MODEL : str = "glm-4.5-flash";
+    pub const MODEL_LIMIT : u16 = 128;
+    pub const MODEL_PROBE_MAX_TOKENS : u16 = 512;
+    pub const MODEL_CHECK_ATTEMPTS : u16 = 2;
+    pub const MODEL_LIST_LIMIT : u16 = 500;
+    pub const MODEL_LIST_CACHE_SECONDS : u16 = 600;
+
+    fn trusted_proxy(configured_bytes: u32, token_matches: bool, subject_valid: bool) -> bool {
+        return configured_bytes >= PROXY_TOKEN_MIN_BYTES && token_matches && subject_valid;
+    }
+
+    fn source_matches_index(managed: bool, index: i32) -> bool {
+        return (managed && index < 0) || (!managed && index >= 0);
+    }
+
+    fn may_add_key(owned: u32) -> bool {
+        return owned < MAX_KEYS_PER_OWNER;
+    }
+
+    fn may_start_probe(active: u32) -> bool {
+        return active < MAX_PROBES_IN_FLIGHT;
+    }
+
+    fn cooldown_passed(has_probe: bool, elapsed_seconds: u32) -> bool {
+        return !has_probe || elapsed_seconds > PROBE_COOLDOWN_SECONDS;
+    }
+
+    fn credential_fits(bytes: u32, printable_without_space: bool) -> bool {
+        return bytes > 0 && bytes <= MAX_KEY_BYTES && printable_without_space;
+    }
+
+    fn same_binding(subject_matches: bool, fingerprint_matches: bool) -> bool {
+        return subject_matches && fingerprint_matches;
+    }
+
+    fn enabled_after_probe(was_enabled: bool, invalid: bool, success: bool, request_enable: bool, same_revision: bool) -> bool {
+        if invalid { return false; }
+        if success && request_enable && same_revision { return true; }
+        return was_enabled;
+    }
+
+    fn model_fits(chars: u32, allowed_chars: bool) -> bool {
+        return chars > 0 && chars <= MODEL_LIMIT && allowed_chars;
+    }
+
+    fn may_switch_model(owns_provider_key: bool, called_tool: bool) -> bool {
+        return owns_provider_key && called_tool;
+    }
+
+    fn may_try_model_check(attempt: u32) -> bool {
+        return attempt < MODEL_CHECK_ATTEMPTS;
+    }
+
+    fn wallet_credit(added: u32) -> u32 {
+        if CREDIT_WALLET { return added; }
+        return 0;
+    }
+
+    test "only a configured matching proxy and valid person subject are trusted" {
+        assert(trusted_proxy(32, true, true));
+        assert(!trusted_proxy(31, true, true));
+        assert(!trusted_proxy(32, false, true));
+        assert(!trusted_proxy(32, true, false));
+    }
+
+    test "managed indices are negative and environment history keeps its nonnegative indices" {
+        assert(source_matches_index(true, -1));
+        assert(!source_matches_index(true, 0));
+        assert(!source_matches_index(false, -1));
+        assert(source_matches_index(false, 0));
+        assert(source_matches_index(false, 10000));
+        assert(!ENVIRONMENT_INDICES_COMPACTED);
+    }
+
+    test "the owner key limit is checked before another key is stored" {
+        assert(may_add_key(0));
+        assert(may_add_key(99));
+        assert(!may_add_key(100));
+    }
+
+    test "probe fanout and per-key cooldown are bounded" {
+        assert(may_start_probe(3));
+        assert(!may_start_probe(4));
+        assert(cooldown_passed(false, 0));
+        assert(!cooldown_passed(true, 59));
+        assert(!cooldown_passed(true, 60));
+        assert(cooldown_passed(true, 61));
+    }
+
+    test "credentials are bounded nonempty printable values" {
+        assert(credential_fits(1, true));
+        assert(credential_fits(4096, true));
+        assert(!credential_fits(0, true));
+        assert(!credential_fits(4097, true));
+        assert(!credential_fits(32, false));
+    }
+
+    test "an existing binding cannot change its owner or credential" {
+        assert(same_binding(true, true));
+        assert(!same_binding(false, true));
+        assert(!same_binding(true, false));
+        assert(!OWNER_REASSIGNMENT);
+    }
+
+    test "a failed probe disables an invalid key and a concurrent disable wins" {
+        assert(!enabled_after_probe(true, true, false, false, true));
+        assert(enabled_after_probe(false, false, true, true, true));
+        assert(!enabled_after_probe(false, false, true, true, false));
+        assert(!enabled_after_probe(false, false, true, false, true));
+        assert(enabled_after_probe(true, false, false, false, true));
+    }
+
+    test "adding credentials never credits a wallet" {
+        assert(wallet_credit(1) == 0);
+        assert(wallet_credit(100) == 0);
+    }
+
+    test "a model name is a bounded nonempty identifier" {
+        assert(model_fits(1, true));
+        assert(model_fits(128, true));
+        assert(!model_fits(0, true));
+        assert(!model_fits(129, true));
+        assert(!model_fits(12, false));
+    }
+
+    test "a model switch needs an owned key of the provider and a tool call" {
+        assert(may_switch_model(true, true));
+        assert(!may_switch_model(false, true));
+        assert(!may_switch_model(true, false));
+    }
+
+    test "a busy or refused key passes the model check to the next one, a bounded number of times" {
+        assert(may_try_model_check(0));
+        assert(may_try_model_check(1));
+        assert(!may_try_model_check(2));
+    }
+
+    test "a probe waits long enough for a slow reasoning model" {
+        assert(PROBE_TIMEOUT_MS >= 60000);
+        assert(MODEL_PROBE_MAX_TOKENS > PROBE_MAX_TOKENS);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 2);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/one-task-three-kinds/card.png b/apps/website/public/ru/learn/one-task-three-kinds/card.png new file mode 100644 index 0000000000..bc3b7d559d Binary files /dev/null and b/apps/website/public/ru/learn/one-task-three-kinds/card.png differ diff --git a/apps/website/public/ru/learn/one-task-three-kinds/index.html b/apps/website/public/ru/learn/one-task-three-kinds/index.html new file mode 100644 index 0000000000..c832e3b23a --- /dev/null +++ b/apps/website/public/ru/learn/one-task-three-kinds/index.html @@ -0,0 +1,224 @@ + + + + + + +Одна задача, три вида: курс «Королева и улей», урок 3 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Одна задача, три вида

+

Вы узнаете

Как задача, ревью и фоновая работа становятся одним видом задачи с семью состояниями и как их читает один фильтр.

+

Королева ведёт работу трёх видов: задачи для пчёл, ревью готовой работы и фоновые работы. У каждого вида были свои коды, и ни одна доска не могла показать их вместе. tasks.t27 даёт им 7 общих состояний, пронумерованных как колонки доски, плюс failed. Отменённая работа показывается как dropped, и так же — забытая строка ревью. task_matches — фильтр, который применяют доска, игра и маршрут: бит на вид, бит на состояние, а 0 значит любое.

+

Попробовать

Покажите только упавшие задачи всех видов; затем найдите, какое состояние работы показывается как dropped.

+

Открыть интерактивный урок →

+
One task, three kinds: the filter the board and the game share
One task, three kinds: the filter the board and the game share ↗
+

An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through.

+

specs/queen/tasks.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/tasks.t27 -- one task, many kinds, and the bees working on them, as everyone reads them
+; Owner, 2026-10-08 (translated): lower every task of the project to the scheduler's native level; a
+; dashboard shows all of it with any filter (https://app.t27.ai/game/kanban); the bees fly over the
+; GitHub issue each is working on in the game (https://app.t27.ai/game). Epic #7718, slice 1.
+; Measured that morning, four surveys:
+; - there is no single task. An issue dispatch (dispatch.t27 COL_*), a job (jobs.t27 J_*) and an app
+;   review (app.t27, per head) each kept their own codes.
+; - no public field said which bee is on which issue. The game drew a public snapshot with no bees, and
+;   its older comb paired the k-th busy slot with the k-th running card, which is a rank, not a link.
+; This card is the view everyone reads: the supervisor builds GET /queen/public-tasks with it, and the
+; board and the game filter and draw with it. The kinds keep their own machines; this only says how
+; each one shows.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenTasks;
+
+/// Kinds of task. Index into KIND_NAMES.
+pub const TK_ISSUE : u8 = 0;
+pub const TK_REVIEW : u8 = 1;
+pub const TK_JOB : u8 = 2;
+pub const TASK_KINDS : u8 = 3;
+pub const KIND_NAMES : [3]str = ["issue", "review", "job"];
+
+/// One state for every kind, numbered as dispatch.t27's board columns (COL_BACKLOG .. COL_DROPPED) so
+/// an issue's column IS its state, plus FAILED, which a column never was. Index into STATE_NAMES.
+pub const TS_BACKLOG : u8 = 0;
+pub const TS_BLOCKED : u8 = 1;
+pub const TS_RUNNING : u8 = 2;
+pub const TS_REVIEW : u8 = 3;
+pub const TS_DONE : u8 = 4;
+pub const TS_DROPPED : u8 = 5;
+pub const TS_FAILED : u8 = 6;
+pub const TASK_STATES : u8 = 7;
+pub const STATE_NAMES : [7]str = ["backlog", "blocked", "running", "review", "done", "dropped", "failed"];
+
+/// A job's state (jobs.t27: 0 running, 1 done, 2 failed, 3 cancelled) as a task state.
+pub fn job_state(j: u8) -> u8 {
+    if (j == 0) { return TS_RUNNING; }
+    if (j == 1) { return TS_DONE; }
+    if (j == 2) { return TS_FAILED; }
+    return TS_DROPPED;
+}
+
+/// An app review's row state (queued 0, posted 1, forgotten 2, failed 3) as a task state.
+pub fn review_state(r: u8) -> u8 {
+    if (r == 0) { return TS_BACKLOG; }
+    if (r == 1) { return TS_DONE; }
+    if (r == 2) { return TS_DROPPED; }
+    return TS_FAILED;
+}
+
+/// The A2A v1.0 task state each state is, for the day the Queen answers A2A at her edge (epic #7718
+/// slice 7). A task waiting for a judge or a person is input-required; dropped is canceled.
+pub const A2A_SUBMITTED : u8 = 1;
+pub const A2A_WORKING : u8 = 2;
+pub const A2A_INPUT_REQUIRED : u8 = 3;
+pub const A2A_COMPLETED : u8 = 4;
+pub const A2A_CANCELED : u8 = 5;
+pub const A2A_FAILED : u8 = 6;
+
+pub fn a2a_state(s: u8) -> u8 {
+    if (s == TS_BACKLOG) { return A2A_SUBMITTED; }
+    if (s == TS_RUNNING) { return A2A_WORKING; }
+    if (s == TS_BLOCKED or s == TS_REVIEW) { return A2A_INPUT_REQUIRED; }
+    if (s == TS_DONE) { return A2A_COMPLETED; }
+    if (s == TS_DROPPED) { return A2A_CANCELED; }
+    return A2A_FAILED;
+}
+
+/// A bee is one dispatch still in flight: offered and not claimed yet (QUEUED), heard from within
+/// BEE_QUIET_SECONDS (WORKING), or silent longer (QUIET). The game draws a quiet bee hovering, not
+/// working: a bee that has not spoken for minutes is not shown as busy.
+pub const BS_QUEUED : u8 = 0;
+pub const BS_WORKING : u8 = 1;
+pub const BS_QUIET : u8 = 2;
+pub const BEE_QUIET_SECONDS : u32 = 180;
+pub const BEE_STATE_NAMES : [3]str = ["queued", "working", "quiet"];
+
+/// `claimed`: a runner took the order (or the container started it); `heard`: an event was recorded
+/// for its conversation; `seconds_since`: since that event, or since it started when none was heard.
+pub fn bee_state(claimed: bool, seconds_since: u32) -> u8 {
+    if (claimed == false) { return BS_QUEUED; }
+    if (seconds_since > BEE_QUIET_SECONDS) { return BS_QUIET; }
+    return BS_WORKING;
+}
+
+/// The filter the board, the game and the route all apply: a bit per kind and a bit per state, and 0
+/// meaning "any". Repository and text are matched beside it; they are strings, and equality on
+/// strings is the caller's.
+pub fn task_matches(kind_mask: u32, state_mask: u32, kind: u8, state: u8) -> bool {
+    if (kind >= TASK_KINDS or state >= TASK_STATES) { return false; }
+    if (kind_mask != 0 and (kind_mask & (1 << kind)) == 0) { return false; }
+    if (state_mask != 0 and (state_mask & (1 << state)) == 0) { return false; }
+    return true;
+}
+
+/// Tasks one answer carries at most; the route pages past it.
+pub const TASK_PAGE_MAX : u32 = 2000;
+pub const TASK_PAGE_DEFAULT : u32 = 500;
+
+test a_job_and_a_review_show_as_one_lifecycle {
+    assert job_state(0) == TS_RUNNING;
+    assert job_state(1) == TS_DONE;
+    assert job_state(2) == TS_FAILED;
+    assert job_state(3) == TS_DROPPED;
+    assert review_state(0) == TS_BACKLOG;
+    assert review_state(1) == TS_DONE;
+    assert review_state(2) == TS_DROPPED;
+    assert review_state(3) == TS_FAILED;
+}
+
+test every_state_has_an_a2a_state {
+    assert a2a_state(TS_BACKLOG) == A2A_SUBMITTED;
+    assert a2a_state(TS_RUNNING) == A2A_WORKING;
+    assert a2a_state(TS_BLOCKED) == A2A_INPUT_REQUIRED;
+    assert a2a_state(TS_REVIEW) == A2A_INPUT_REQUIRED;
+    assert a2a_state(TS_DONE) == A2A_COMPLETED;
+    assert a2a_state(TS_DROPPED) == A2A_CANCELED;
+    assert a2a_state(TS_FAILED) == A2A_FAILED;
+}
+
+test a_bee_is_queued_working_or_quiet {
+    assert bee_state(false, 0) == BS_QUEUED;
+    assert bee_state(false, 9999) == BS_QUEUED;
+    assert bee_state(true, 0) == BS_WORKING;
+    assert bee_state(true, BEE_QUIET_SECONDS) == BS_WORKING;
+    assert bee_state(true, BEE_QUIET_SECONDS + 1) == BS_QUIET;
+}
+
+test a_filter_narrows_and_zero_means_any {
+    assert task_matches(0, 0, TK_ISSUE, TS_RUNNING) == true;
+    assert task_matches(1 << TK_ISSUE, 0, TK_ISSUE, TS_DONE) == true;
+    assert task_matches(1 << TK_JOB, 0, TK_ISSUE, TS_DONE) == false;
+    assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_RUNNING) == true;
+    assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_BACKLOG) == false;
+    assert task_matches((1 << TK_ISSUE) | (1 << TK_JOB), (1 << TS_RUNNING) | (1 << TS_REVIEW), TK_JOB, TS_REVIEW) == true;
+    // an unknown kind or state matches nothing, even with no filter
+    assert task_matches(0, 0, TASK_KINDS, TS_DONE) == false;
+    assert task_matches(0, 0, TK_ISSUE, TASK_STATES) == false;
+}
+
+invariant the_states_are_the_board_columns {
+    assert TS_BACKLOG == 0 and TS_BLOCKED == 1 and TS_RUNNING == 2;
+    assert TS_REVIEW == 3 and TS_DONE == 4 and TS_DROPPED == 5 and TS_FAILED == 6;
+    assert TK_ISSUE == 0 and TK_REVIEW == 1 and TK_JOB == 2;
+    assert TASK_PAGE_DEFAULT <= TASK_PAGE_MAX;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/ppo-and-the-reward/index.html b/apps/website/public/ru/learn/ppo-and-the-reward/index.html index e846b27c3e..952562832a 100644 --- a/apps/website/public/ru/learn/ppo-and-the-reward/index.html +++ b/apps/website/public/ru/learn/ppo-and-the-reward/index.html @@ -263,7 +263,7 @@

PPO и награда

invariant ppo_clip_loss_constraint_4 assert clipped_surrogate(0.1, -1.0, 0.2) == -0.8

Открыть спеку урока в плеере ↗

- +

Все уроки

diff --git a/apps/website/public/ru/learn/queen-and-hive/card.png b/apps/website/public/ru/learn/queen-and-hive/card.png new file mode 100644 index 0000000000..70a2b89653 Binary files /dev/null and b/apps/website/public/ru/learn/queen-and-hive/card.png differ diff --git a/apps/website/public/ru/learn/queen-and-hive/index.html b/apps/website/public/ru/learn/queen-and-hive/index.html new file mode 100644 index 0000000000..123d4b8aad --- /dev/null +++ b/apps/website/public/ru/learn/queen-and-hive/index.html @@ -0,0 +1,72 @@ + + + + + + +Королева и улей: как проект t27 строит себя сам · t27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+

Курс

+

Королева и улей: как проект t27 строит себя сам

+

Этот проект строит себя сам: задача становится работой для пчелы, пчела пишет спеку t27, пчела-ревьюер её проверяет, спеку сливают, и сайт пересобирается. Королева, которая ведёт этот цикл, записана спеками t27 с тестами, и каждый виджет здесь исполняет тела fn одной из них, скомпилированные в WebAssembly.

+

Курс 11, 27 уроков, в каждом живой виджет и спека t27: оформление задачи, приоритет и сначала ревью, жизнь задачи, ревьюер и ворота слияния, затор ревью, ключи, заслуги и токены, агенты в одной группе, ночной дозор и доска. Каждый ответ даёт скомпилированная спека.

+

Открыть интерактивный курс →

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/review-first/card.png b/apps/website/public/ru/learn/review-first/card.png new file mode 100644 index 0000000000..58842a7612 Binary files /dev/null and b/apps/website/public/ru/learn/review-first/card.png differ diff --git a/apps/website/public/ru/learn/review-first/index.html b/apps/website/public/ru/learn/review-first/index.html new file mode 100644 index 0000000000..8633e6b8b7 --- /dev/null +++ b/apps/website/public/ru/learn/review-first/index.html @@ -0,0 +1,250 @@ + + + + + + +Сначала ревью: курс «Королева и улей», урок 6 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Сначала ревью

+

Вы узнаете

Почему Королева закрывает бэклог, пока готовая работа ждёт ревью, и что всё равно идёт.

+

На 2026-10-18 страница статуса показывала 69 из 70 занятых воркеров, а за час — 102 запуска, 56 завершённых и 11 проверенных. Пчёл запускалось примерно девять на каждый вердикт, и каждая новая пчела держала файлы, нужные пчёлам на ревью, когда их возвращали. review_first.t27 закрывает бэклог, пока очередь ревью длиннее REVIEW_QUEUE_MAX, 0. Работа, которую ревью вернуло, и работа, назначенная владельцем, всё равно идут: admission пропускает их при любой длине очереди.

+

Попробовать

Перетащите очередь на 0 и обратно на 1; затем найдите кандидатов, которых пропускают при любой длине.

+

Открыть интерактивный урок →

+
Review first: no new work while finished work waits
Review first: no new work while finished work waits ↗
+

Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go.

+

specs/queen/review_first.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/review_first.t27 -- no new work from the backlog while finished work waits in review (gHashTag/t27#10555).
+; The owner's rule, 2026-10-18: the Queen takes nothing from the backlog while there is a queue in review.
+; Measured 2026-10-18 17:09Z on api.t27.ai/queen/status: 69 of 70 workers busy; in the last hour 102
+; dispatched, 56 finished, 11 reviewed; reviewable (the review sweep's own queue) 29, unreviewed 362.
+; The swarm starts about nine bees for every verdict, so the queue in review only grows, and each new bee
+; holds files (claim.t27) that the bees already in review need when they are sent back.
+; The rule:
+;   1. the review queue is exactly the review sweep's own filter, the one the status page counts as
+;      `reviewable`: the dispatch started, it finished, its review_state is NULL or 'wait', it was not
+;      reaped, and its issue is still open (in_review_queue);
+;   2. while that queue is longer than REVIEW_QUEUE_MAX (0: not while anything waits) a candidate from
+;      the backlog is refused, and the round says why (admission, R_REVIEW_FIRST);
+;   3. work the review itself asked for is never backlog: an issue the review sent back is dispatched
+;      again at any queue length (KIND_REWORK), and so is an issue the owner assigned by hand
+;      (KIND_ASSIGNED, the supervisor's pendingAssigns);
+;   4. the queue empties on its own: every outcome of the review that is not an accept has a clock
+;      (review_valve.t27), so a closed backlog always reopens without a person.
+; The host counts the queue and names each candidate's kind; this file decides. Not here: the wiring into
+; trios (queen-tick.ts candidate filter, vendored wasm and t27c gen-ts constants), how keys are shared
+; between bees and the reviewer, the review verdicts themselves. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenReviewFirst;
+
+; --- 1. What a candidate is ---------------------------------------------------------------------------
+
+/// KIND_BACKLOG: an open issue with no dispatch the review is working on. KIND_REWORK: an issue whose
+/// last dispatch the review sent back (review_state 'sendBack'). KIND_ASSIGNED: an issue the owner
+/// assigned by hand. The codes are mirrored by number: a new kind takes the next code, none moves.
+pub const KIND_BACKLOG : u8 = 0;
+pub const KIND_REWORK : u8 = 1;
+pub const KIND_ASSIGNED : u8 = 2;
+
+/// The owner's hand wins over the review's send-back, and both win over the backlog.
+pub fn kind_of(assigned: bool, sent_back: bool) -> u8 {
+    if (assigned) { return KIND_ASSIGNED; }
+    if (sent_back) { return KIND_REWORK; }
+    return KIND_BACKLOG;
+}
+
+; --- 2. What the review queue is ----------------------------------------------------------------------
+
+/// A dispatch row's review_state as the host reads it: RS_NONE is NULL (no verdict yet), RS_WAIT is
+/// 'wait' (fewer verdicts than criteria, dispatch.t27 REVIEW_RULE), RS_OTHER is any written verdict.
+pub const RS_NONE : u8 = 0;
+pub const RS_WAIT : u8 = 1;
+pub const RS_OTHER : u8 = 2;
+
+/// Whether one dispatch row is in the review queue: the sweep's own filter (trios queen-tick.ts
+/// reviewFinishedDispatches, counted as `reviewable` by queen-public-status.ts). A row whose issue was
+/// closed is never reviewed, so it never holds the backlog shut.
+pub fn in_review_queue(started: bool, finished: bool, state: u8, reaped: bool, issue_open: bool) -> bool {
+    if (started == false) { return false; }
+    if (finished == false) { return false; }
+    if (reaped) { return false; }
+    if (issue_open == false) { return false; }
+    if (state == RS_NONE) { return true; }
+    return state == RS_WAIT;
+}
+
+; --- 3. Admission -------------------------------------------------------------------------------------
+
+/// How many dispatches may wait in review while the backlog stays open. Zero, the owner's words: not
+/// while anything is in review.
+pub const REVIEW_QUEUE_MAX : u32 = 0;
+
+/// R_ADMIT: the candidate may be chosen. R_REVIEW_FIRST: refused, the backlog is closed while the review
+/// queue is longer than REVIEW_QUEUE_MAX.
+pub const R_ADMIT : u8 = 0;
+pub const R_REVIEW_FIRST : u8 = 1;
+
+/// The refusal as the round reports it, after the queue length: "29 dispatches wait in review; ...".
+pub const REFUSAL_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty";
+
+pub fn backlog_open(review_queue: u32) -> bool {
+    return review_queue <= REVIEW_QUEUE_MAX;
+}
+
+pub fn admission(kind: u8, review_queue: u32) -> u8 {
+    if (kind == KIND_REWORK) { return R_ADMIT; }
+    if (kind == KIND_ASSIGNED) { return R_ADMIT; }
+    if (backlog_open(review_queue)) { return R_ADMIT; }
+    return R_REVIEW_FIRST;
+}
+
+pub fn admits(kind: u8, review_queue: u32) -> bool {
+    return admission(kind, review_queue) == R_ADMIT;
+}
+
+/// Whether a round that admitted no candidate owes the review-first sentence: some candidate was
+/// refused by this rule and none was admitted.
+pub fn round_refused(admitted: u32, refused: u32) -> bool {
+    if (admitted > 0) { return false; }
+    return refused > 0;
+}
+
+; --- 4. Tests -----------------------------------------------------------------------------------------
+
+test the_owner_hand_wins_then_the_review_then_the_backlog {
+    assert kind_of(false, false) == 0;
+    assert kind_of(false, true) == 1;
+    assert kind_of(true, false) == 2;
+    assert kind_of(true, true) == 2;
+}
+
+test the_queue_is_the_sweeps_own_filter {
+    assert in_review_queue(true, true, RS_NONE, false, true) == true;
+    assert in_review_queue(true, true, RS_WAIT, false, true) == true;
+    assert in_review_queue(true, true, RS_OTHER, false, true) == false;
+    assert in_review_queue(true, false, RS_NONE, false, true) == false;
+    assert in_review_queue(false, true, RS_NONE, false, true) == false;
+    assert in_review_queue(true, true, RS_NONE, true, true) == false;
+    assert in_review_queue(true, true, RS_NONE, false, false) == false;
+    assert in_review_queue(true, true, RS_WAIT, true, true) == false;
+    assert in_review_queue(true, true, 7, false, true) == false;
+}
+
+test the_backlog_is_closed_while_anything_waits_in_review {
+    assert admission(KIND_BACKLOG, 0) == 0;
+    assert admission(KIND_BACKLOG, 1) == 1;
+    assert admission(KIND_BACKLOG, 29) == 1;
+    assert admission(KIND_BACKLOG, 4294967295) == 1;
+    assert backlog_open(0) == true;
+    assert backlog_open(1) == false;
+    assert admits(KIND_BACKLOG, 0) == true;
+    assert admits(KIND_BACKLOG, 1) == false;
+}
+
+test work_the_review_asked_for_is_never_backlog {
+    assert admission(KIND_REWORK, 0) == 0;
+    assert admission(KIND_REWORK, 1) == 0;
+    assert admission(KIND_REWORK, 29) == 0;
+    assert admission(KIND_REWORK, 4294967295) == 0;
+    assert admits(KIND_REWORK, 362) == true;
+}
+
+test the_owners_assignment_is_never_held {
+    assert admission(KIND_ASSIGNED, 0) == 0;
+    assert admission(KIND_ASSIGNED, 29) == 0;
+    assert admits(KIND_ASSIGNED, 4294967295) == true;
+}
+
+test an_unknown_kind_is_backlog_and_is_held {
+    assert admission(3, 0) == 0;
+    assert admission(3, 1) == 1;
+    assert admission(255, 29) == 1;
+}
+
+test a_round_reports_the_rule_only_when_it_stopped_everything {
+    assert round_refused(0, 0) == false;
+    assert round_refused(0, 5) == true;
+    assert round_refused(1, 5) == false;
+    assert round_refused(3, 0) == false;
+}
+
+test the_measured_round_of_2026_10_18 {
+    assert admits(kind_of(false, false), 29) == false;
+    assert admits(kind_of(false, true), 29) == true;
+    assert admits(kind_of(true, false), 29) == true;
+    assert admits(kind_of(false, false), 0) == true;
+}
+
+; --- 5. Invariants ------------------------------------------------------------------------------------
+
+invariant the_codes_never_move {
+    assert KIND_BACKLOG == 0 and KIND_REWORK == 1 and KIND_ASSIGNED == 2;
+    assert RS_NONE == 0 and RS_WAIT == 1 and RS_OTHER == 2;
+    assert R_ADMIT == 0 and R_REVIEW_FIRST == 1;
+}
+
+/// The owner's bound: nothing may wait in review while the backlog is open.
+invariant the_bound_is_the_owners {
+    assert REVIEW_QUEUE_MAX == 0;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/shaping-an-issue/card.png b/apps/website/public/ru/learn/shaping-an-issue/card.png new file mode 100644 index 0000000000..730175fb60 Binary files /dev/null and b/apps/website/public/ru/learn/shaping-an-issue/card.png differ diff --git a/apps/website/public/ru/learn/shaping-an-issue/index.html b/apps/website/public/ru/learn/shaping-an-issue/index.html new file mode 100644 index 0000000000..4aaa307266 --- /dev/null +++ b/apps/website/public/ru/learn/shaping-an-issue/index.html @@ -0,0 +1,205 @@ + + + + + + +Оформление задачи: курс «Королева и улей», урок 1 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Оформление задачи

+

Вы узнаете

Что Королева делает с задачей без нужных разделов и почему останавливается после двух попыток.

+

Пчела может начать только задачу, в которой сказано, что сделать и каких файлов можно касаться. Королева проверяет каждую открытую задачу функцией judge() из specs/policy/issue_shape.t27. Неготовую она оформляет: модель дописывает недостающие разделы, и текст возвращается в задачу, только если judge() больше ничего не находит. После SHAPE_ATTEMPT_LIMIT, 2 неудачных попыток, она останавливается и один раз пишет комментарий, чего не хватает, а эпик не оформляется никогда. Решает shape_action в shaping.t27, а новая попытка ждёт 360 минут.

+

Попробовать

Поставьте два недостающих раздела и пройдите попытки; затем отметьте, что автору уже написали, и посмотрите, в какую колонку попадёт задача.

+

Открыть интерактивный урок →

+
Shaping: what the Queen does with an issue that is not ready
Shaping: what the Queen does with an issue that is not ready ↗
+

Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing.

+

specs/queen/shaping.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks
+; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix
+; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete.
+; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most
+; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as
+; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703,
+; #7642), which the rule counts as missing.
+; --
+; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section).
+; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a
+; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either
+; SHAPED by her (the missing sections are written by a model, every boundary path is checked against
+; the repository, and the body is written back only if judge() then returns 0), or, when shaping
+; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing.
+; --
+; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the
+; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is
+; accepted only when the issue itself cites it and its directory exists, and never when it would be a
+; new file in a language the only-t27 rule denies.
+; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenShaping;
+
+/// What the Queen does with one open issue.
+///   LEAVE  nothing: it is ready, a container, or already told
+///   SHAPE  ask for the missing sections, judge them, write them back if they pass
+///   TELL   comment once, naming what is missing
+pub const SH_LEAVE : u8 = 0;
+pub const SH_SHAPE : u8 = 1;
+pub const SH_TELL : u8 = 2;
+
+/// Attempts at shaping one issue before its author is told instead.
+pub const SHAPE_ATTEMPT_LIMIT : u32 = 2;
+/// Issues shaped per round. A shape is one model call; the round has other work.
+pub const SHAPES_PER_ROUND : u32 = 3;
+/// Minutes between two attempts on one issue, so a refused shape is not retried every round.
+pub const SHAPE_RETRY_MINUTES : u32 = 360;
+/// A boundary is a reservation; one naming more files than this is not a task, it is a project.
+pub const MAX_BOUNDARY_PATHS : u32 = 12;
+
+/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap;
+/// `attempts`: shapes tried so far; `told`: the author was already told.
+pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 {
+    if (missing == 0) { return SH_LEAVE; }
+    if (container) { return SH_LEAVE; }
+    if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; }
+    if (told) { return SH_LEAVE; }
+    return SH_TELL;
+}
+
+/// Is one proposed boundary path acceptable?
+///   exists       the file is in the repository at the base ref
+///   dir_exists   its directory is (a new file beside its siblings)
+///   cited        the issue's own title or body names this path
+///   foreign_new  it would be a NEW file whose extension the only-t27 rule denies to new work
+/// A file that exists is always acceptable: finding the file a title describes is the point. A file
+/// that does not exist must be cited by the issue, in a directory that exists, and not foreign.
+pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool {
+    if (exists) { return true; }
+    if (dir_exists == false) { return false; }
+    if (foreign_new) { return false; }
+    return cited;
+}
+
+/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and
+/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted.
+pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool {
+    if (missing_after != 0) { return false; }
+    if (paths == 0) { return false; }
+    if (paths > MAX_BOUNDARY_PATHS) { return false; }
+    return rejected == 0;
+}
+
+/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue.
+pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool {
+    if (attempts == 0) { return true; }
+    return minutes_since_last >= SHAPE_RETRY_MINUTES;
+}
+
+test a_ready_issue_and_a_container_are_left_alone {
+    assert shape_action(0, false, 0, false) == SH_LEAVE;
+    assert shape_action(15, true, 0, false) == SH_LEAVE;
+    assert shape_action(1, true, 9, false) == SH_LEAVE;
+}
+
+test an_unready_issue_is_shaped_then_its_author_is_told_once {
+    assert shape_action(1, false, 0, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL;
+    assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE;
+    assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE;
+}
+
+test a_boundary_path_must_be_true_of_the_repository {
+    // an existing file, cited or found
+    assert path_accepted(true, true, false, false) == true;
+    assert path_accepted(true, true, true, false) == true;
+    // a new spec the issue asks for, beside its siblings
+    assert path_accepted(false, true, true, false) == true;
+    // a new file the issue never mentioned: invented
+    assert path_accepted(false, true, false, false) == false;
+    // a new file in a directory that does not exist
+    assert path_accepted(false, false, true, false) == false;
+    // a new .py or .ts the only-t27 rule denies, even when cited
+    assert path_accepted(false, true, true, true) == false;
+}
+
+test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds {
+    assert shape_lands(0, 1, 0) == true;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true;
+    assert shape_lands(1, 3, 0) == false;
+    assert shape_lands(0, 0, 0) == false;
+    assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false;
+    assert shape_lands(0, 3, 1) == false;
+}
+
+test a_refused_shape_is_not_retried_every_round {
+    assert attempt_due(0, 0) == true;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false;
+    assert attempt_due(1, SHAPE_RETRY_MINUTES) == true;
+}
+
+invariant the_codes_and_limits_never_move {
+    assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2;
+    assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0;
+    assert MAX_BOUNDARY_PATHS > 0;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/stop-and-mute/card.png b/apps/website/public/ru/learn/stop-and-mute/card.png new file mode 100644 index 0000000000..00dff80abc Binary files /dev/null and b/apps/website/public/ru/learn/stop-and-mute/card.png differ diff --git a/apps/website/public/ru/learn/stop-and-mute/index.html b/apps/website/public/ru/learn/stop-and-mute/index.html new file mode 100644 index 0000000000..923061de21 --- /dev/null +++ b/apps/website/public/ru/learn/stop-and-mute/index.html @@ -0,0 +1,436 @@ + + + + + + +Stop и mute: курс «Королева и улей», урок 24 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Stop и mute

+

Вы узнаете

Что делают /stop и /mute и почему ни один не теряет человека или прямой вызов.

+

cooled говорит, прошло ли 5000 мс с последнего сообщения бота. /stop глушит разговоры ботов между собой в чате на STOP_WINDOW_MS, 10 минут, а человека по-прежнему слышно: это may_answer_after_stop. /mute заставляет агента молчать в теме MUTE_MS, час, но на свой тег он отвечает: heard_while_muted. Оба — часы, поэтому никому не нужно включать улей обратно.

+

Попробовать

Протяните часы /stop дальше 10 минут; затем позовите тегом заглушённого агента.

+

Открыть интерактивный урок →

+
The cooldown clock, /stop and /mute
The cooldown clock, /stop and /mute ↗
+

One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-comb-answers/card.png b/apps/website/public/ru/learn/the-comb-answers/card.png new file mode 100644 index 0000000000..bb3fac6aa6 Binary files /dev/null and b/apps/website/public/ru/learn/the-comb-answers/card.png differ diff --git a/apps/website/public/ru/learn/the-comb-answers/index.html b/apps/website/public/ru/learn/the-comb-answers/index.html new file mode 100644 index 0000000000..0ad12bafb9 --- /dev/null +++ b/apps/website/public/ru/learn/the-comb-answers/index.html @@ -0,0 +1,259 @@ + + + + + + +Соты отвечают: курс «Королева и улей», урок 27 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Соты отвечают

+

Вы узнаете

Как игра показывает, что с задачей что-то случилось, и как часто растёт карта.

+

В сотах игры у каждой задачи своя ячейка, и до hive.t27 принятая задача, потерянная аренда и отмена выглядели так же, как отсутствие новостей. cell_effect даёт каждому из 10 видов событий свою краску: старт белый, принявшее ревью золотое, возврат янтарный, потерянная аренда красная, отмена серая. flash_alpha гасит вспышку за FLASH_MS, 1800 мс. Новая ячейка перестраивает сцену, поэтому cells_due ставит новые ячейки пачками не чаще раза в 300 с.

+

Попробовать

Нажмите ячейку ревью и переключите вердикт; затем найдите момент, когда вспышка гаснет.

+

Открыть интерактивный урок →

+
The comb answers: what an event does to a cell
The comb answers: what an event does to a cell ↗
+

Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells.

+

specs/queen/hive.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to
+; a cell, how a ship leaves, and how often the map may grow
+; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game
+; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4.
+; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day):
+; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen.
+; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far
+;   from the repository it works for.
+; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole
+;   scene: one rebuild for most bees that started.
+; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same
+;   as no news.
+; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two
+; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenHive;
+
+pub const HIVE_KINDS : u8 = 10;
+
+; --- 1. Where a ship flies ---------------------------------------------------------------------------
+
+/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this
+/// map can show (a repository with no region, or a task that is not an issue).
+pub const CELL_PLACED : u8 = 0;
+pub const CELL_PENDING : u8 = 1;
+pub const CELL_NONE : u8 = 2;
+
+/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim,
+/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is
+/// placed. HOME: flying back to the entrance, its task over.
+pub const SM_PARK : u8 = 0;
+pub const SM_ORBIT : u8 = 1;
+pub const SM_HOVER : u8 = 2;
+pub const SM_PORTAL : u8 = 3;
+pub const SM_HOME : u8 = 4;
+
+/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no
+/// longer in the feed.
+pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 {
+    if (departing) { return SM_HOME; }
+    if (queued) { return SM_PARK; }
+    if (cell == CELL_PENDING) { return SM_PORTAL; }
+    if (cell != CELL_PLACED) { return SM_PARK; }
+    if (working) { return SM_ORBIT; }
+    return SM_HOVER;
+}
+
+; --- 2. How a ship leaves ----------------------------------------------------------------------------
+
+/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing.
+pub const DEPART_MS : u32 = 2400;
+
+pub fn ship_kept(ms_since_left: u32) -> bool {
+    return ms_since_left < DEPART_MS;
+}
+
+/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing.
+pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 {
+    if (departing == false) { return 1000; }
+    if (ms_since_left >= DEPART_MS) { return 0; }
+    return 1000 - (ms_since_left * 1000) / DEPART_MS;
+}
+
+; --- 3. What an event does to a cell -----------------------------------------------------------------
+
+/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the
+/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is
+/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it.
+pub const FX_NONE : u8 = 0;
+pub const FX_START : u8 = 1;
+pub const FX_ACCEPT : u8 = 2;
+pub const FX_SEND_BACK : u8 = 3;
+pub const FX_LOST : u8 = 4;
+pub const FX_CANCEL : u8 = 5;
+
+/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by
+/// its verdict, cancel CANCEL; everything else nothing.
+pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0];
+
+/// `accepted`: a review's verdict was an acceptance. Only a review reads it.
+pub fn cell_effect(kind: u8, accepted: bool) -> u8 {
+    if (kind >= HIVE_KINDS) { return FX_NONE; }
+    let fx : u8 = FX_OF_KIND[kind];
+    if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; }
+    return fx;
+}
+
+/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn.
+pub const FLASH_MS : u32 = 1800;
+
+pub fn flash_alpha(ms_since: u32) -> u32 {
+    if (ms_since >= FLASH_MS) { return 0; }
+    return 1000 - (ms_since * 1000) / FLASH_MS;
+}
+
+; --- 4. How often the map may grow -------------------------------------------------------------------
+
+/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding
+/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS --
+/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of
+/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's
+/// portal (SM_PORTAL), so where it is going is already true.
+pub const CELL_BATCH_SECONDS : u32 = 300;
+
+pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool {
+    if (pending == 0) { return false; }
+    if (placed_before == false) { return true; }
+    return seconds_since_batch >= CELL_BATCH_SECONDS;
+}
+
+test a_ship_flies_where_its_bee_is {
+    // queued: at the entrance, wherever its cell stands
+    assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK;
+    assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK;
+    // claimed, its cell placed: orbit while heard, hover when quiet
+    assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT;
+    assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER;
+    // its cell not placed yet: at its repository's portal, working or not
+    assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL;
+    assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL;
+    // no cell this map can show: back at the entrance
+    assert ship_mode(false, true, CELL_NONE, false) == SM_PARK;
+    // gone from the feed: home, whatever it was
+    assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME;
+    assert ship_mode(true, false, CELL_NONE, true) == SM_HOME;
+}
+
+test a_ship_leaves_by_flying_home {
+    assert ship_kept(0) == true;
+    assert ship_kept(DEPART_MS - 1) == true;
+    assert ship_kept(DEPART_MS) == false;
+    assert ship_alpha(false, 0) == 1000;
+    assert ship_alpha(false, 99999) == 1000;
+    assert ship_alpha(true, 0) == 1000;
+    assert ship_alpha(true, 1200) == 500;
+    assert ship_alpha(true, DEPART_MS - 1) == 1;
+    assert ship_alpha(true, DEPART_MS) == 0;
+    assert ship_alpha(true, 4000000000) == 0;
+}
+
+test an_event_lights_its_cell {
+    assert cell_effect(0, false) == FX_START;
+    assert cell_effect(1, true) == FX_NONE;
+    assert cell_effect(2, false) == FX_NONE;
+    assert cell_effect(3, false) == FX_LOST;
+    assert cell_effect(4, true) == FX_NONE;
+    assert cell_effect(5, true) == FX_ACCEPT;
+    assert cell_effect(5, false) == FX_SEND_BACK;
+    assert cell_effect(6, true) == FX_NONE;
+    assert cell_effect(7, false) == FX_CANCEL;
+    assert cell_effect(8, false) == FX_NONE;
+    assert cell_effect(9, true) == FX_NONE;
+    // only a review reads the verdict
+    assert cell_effect(3, true) == FX_LOST;
+    assert cell_effect(HIVE_KINDS, true) == FX_NONE;
+}
+
+test a_flash_fades {
+    assert flash_alpha(0) == 1000;
+    assert flash_alpha(900) == 500;
+    assert flash_alpha(FLASH_MS - 1) == 1;
+    assert flash_alpha(FLASH_MS) == 0;
+    assert flash_alpha(4000000000) == 0;
+}
+
+test the_map_grows_in_batches {
+    assert cells_due(0, false, 0) == false;
+    assert cells_due(0, true, 99999) == false;
+    // the first answer places every cell at once
+    assert cells_due(3, false, 0) == true;
+    // after that, one batch a period
+    assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false;
+    assert cells_due(1, true, CELL_BATCH_SECONDS) == true;
+}
+
+invariant one_row_per_kind {
+    assert HIVE_KINDS == 10;
+    assert FX_OF_KIND[5] == FX_ACCEPT;
+    assert FLASH_MS < DEPART_MS * 2;
+    assert CELL_BATCH_SECONDS == 300;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-merge-gate/card.png b/apps/website/public/ru/learn/the-merge-gate/card.png new file mode 100644 index 0000000000..9d76f9afe1 Binary files /dev/null and b/apps/website/public/ru/learn/the-merge-gate/card.png differ diff --git a/apps/website/public/ru/learn/the-merge-gate/index.html b/apps/website/public/ru/learn/the-merge-gate/index.html new file mode 100644 index 0000000000..f1bf2524c5 --- /dev/null +++ b/apps/website/public/ru/learn/the-merge-gate/index.html @@ -0,0 +1,377 @@ + + + + + + +Ворота слияния: курс «Королева и улей», урок 12 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Ворота слияния

+

Вы узнаете

Когда мерджер может слить пулл-реквест, который одобрила пчела-ревьюер.

+

Пчела-ревьюер одобряет, мерджер сливает; merger_gate.t27 — правило между ними. check_passes пропускает зелёную проверку и задерживает любую, которая ещё идёт. Красная проходит, только если она не обязательная, завершилась и пчела назвала её списанной, а если набор правил не читается, обязательной считается каждая проверка. gate_open нужен хотя бы 1 опубликованный результат, 0 идущих и 0 блокирующих: ноль проверок — не ноль падений.

+

Попробовать

Дайте идущей проверке завершиться зелёной; затем сделайте набор правил нечитаемым и посмотрите на списанную красную.

+

Открыть интерактивный урок →

+
The merge gate: when an approved pull request may merge
The merge gate: when an approved pull request may merge ↗
+

Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks.

+

specs/queen/merger_gate.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved
+; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq).
+; It does not import this spec, and t27c gen-js lowers no function bodies (#7300).
+; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario
+; asserts the verdict that scenario pins for the workflow.
+; Negative controls: discounted required check, unconcluded check, required check never posted,
+; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run
+; of the same check was CANCELLED (#7317)
+; ASCII only (L3)
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenMergerGate;
+
+/// With the ruleset unreadable the merger cannot tell which checks are required,
+/// so every check counts as required and no red one is discountable (#5776 item 2).
+pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool {
+    return !ruleset_readable || required;
+}
+
+/// A red check passes only when it is not required, has concluded, and is discounted
+pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool {
+    return !required && concluded && discounted;
+}
+
+/// A required check passes when it is posted and green
+pub fn required_check_passes(posted: bool, green: bool) -> bool {
+    return posted && green;
+}
+
+/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks;
+/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check
+/// that does not count as required.
+pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool {
+    if (!concluded) { return false; }
+    if (green) { return true; }
+    return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted);
+}
+
+// ---------------------------------------------------------------------------
+// What master says about a discounted red (#7317)
+//
+// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its
+// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled
+// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it.
+// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the
+// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or
+// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts.
+
+/// One master run of the red check, as the merger reads its job's status and conclusion.
+/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code.
+pub const RUN_MISSING : u8 = 0;
+pub const RUN_FAILURE : u8 = 1;
+pub const RUN_SUCCESS : u8 = 2;
+pub const RUN_CANCELLED : u8 = 3;
+pub const RUN_SKIPPED : u8 = 4;
+pub const RUN_TIMED_OUT : u8 = 5;
+/// queued or in progress
+pub const RUN_RUNNING : u8 = 6;
+/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail
+pub const RUN_OTHER : u8 = 7;
+
+/// What master says about the PR's red.
+pub const MASTER_UNKNOWN : u8 = 0;
+/// a completed failure: the red is inherited
+pub const MASTER_RED : u8 = 1;
+/// a completed success: master was green, the red is the PR's own
+pub const MASTER_GREEN : u8 = 2;
+
+/// One run on its own. Only a completed failure or a completed success decides anything.
+pub fn run_says(run: u8) -> u8 {
+    if (run == RUN_FAILURE) { return MASTER_RED; }
+    if (run == RUN_SUCCESS) { return MASTER_GREEN; }
+    return MASTER_UNKNOWN;
+}
+
+/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN.
+/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not.
+/// A run older than the PR's merge base is not evidence about the code the PR started from.
+pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 {
+    if (so_far != MASTER_UNKNOWN) { return so_far; }
+    if (!at_or_after_merge_base) { return MASTER_UNKNOWN; }
+    return run_says(run);
+}
+
+/// A red is discounted only when the bee's approval names it AND master says red.
+/// This is the `discounted` that check_passes takes.
+pub fn discount_holds(bee_named: bool, master: u8) -> bool {
+    return bee_named && master == MASTER_RED;
+}
+
+/// The gate opens when at least one check has posted, none is still running,
+/// and none blocks. Zero checks is not zero failures.
+pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool {
+    return posted > 0 && running == 0 && blocking == 0;
+}
+
+// Tests for counts_as_required
+
+test counts_as_required_follows_the_ruleset_when_readable {
+    assert counts_as_required(true, true) == true;
+    assert counts_as_required(true, false) == false;
+}
+
+test counts_as_required_is_every_check_when_unreadable {
+    assert counts_as_required(false, false) == true;
+    assert counts_as_required(false, true) == true;
+}
+
+// Tests for red_check_passes
+
+// Positive case: non-required check that concluded and is discounted
+test red_check_passes_valid_discounted_non_required {
+    assert red_check_passes(false, true, true) == true;
+}
+
+// Negative control: discounted required check (required=true, cannot be discounted)
+test red_check_passes_discounted_required_fails {
+    assert red_check_passes(true, true, true) == false;
+}
+
+// Negative control: unconcluded check (concluded=false, even if discounted)
+test red_check_passes_unconcluded_fails {
+    assert red_check_passes(false, false, true) == false;
+}
+
+// Negative control: non-required check that concluded but not discounted
+test red_check_passes_non_discounted_fails {
+    assert red_check_passes(false, true, false) == false;
+}
+
+test red_check_passes_all_false_fails {
+    assert red_check_passes(true, false, false) == false;
+}
+
+// Tests for required_check_passes
+
+test required_check_passes_posted_and_green_passes {
+    assert required_check_passes(true, true) == true;
+}
+
+// Self-test scenario "required check never posted"
+test required_check_passes_not_posted_fails {
+    assert required_check_passes(false, true) == false;
+}
+
+test required_check_passes_posted_but_not_green_fails {
+    assert required_check_passes(true, false) == false;
+}
+
+test required_check_passes_neither_posted_nor_green_fails {
+    assert required_check_passes(false, false) == false;
+}
+
+// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted)
+
+test check_passes_green_advisory_and_green_required_pass {
+    assert check_passes(true, false, true, true, false) == true;
+    assert check_passes(true, true, true, true, false) == true;
+}
+
+// Self-test scenario "red advisory check, discounted in the bee's approval"
+test check_passes_red_advisory_discounted_passes {
+    assert check_passes(true, false, true, false, true) == true;
+}
+
+// Self-test scenario "red advisory check, not discounted"
+test check_passes_red_advisory_not_discounted_blocks {
+    assert check_passes(true, false, true, false, false) == false;
+}
+
+// Self-test scenario "red REQUIRED check, discounted anyway"
+test check_passes_red_required_discounted_blocks {
+    assert check_passes(true, true, true, false, true) == false;
+}
+
+// Self-test scenarios "a check still running" and "pending commit status blocks":
+// not concluded blocks whatever else is true, a discount included
+test check_passes_running_check_blocks {
+    assert check_passes(true, false, false, true, true) == false;
+    assert check_passes(true, false, false, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable: a discounted red check still blocks"
+test check_passes_unreadable_ruleset_discounted_red_blocks {
+    assert check_passes(false, false, true, false, true) == false;
+}
+
+// Self-test scenario "ruleset unreadable, every check green"
+test check_passes_unreadable_ruleset_green_passes {
+    assert check_passes(false, false, true, true, false) == true;
+}
+
+// Tests for gate_open: (posted, running, blocking)
+
+// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks
+test gate_open_posted_checks_all_pass_opens {
+    assert gate_open(2, 0, 0) == true;
+    assert gate_open(1, 0, 0) == true;
+}
+
+// Negative control: zero checks is not zero failures
+test gate_open_zero_checks_stays_closed {
+    assert gate_open(0, 0, 0) == false;
+}
+
+// Self-test scenario "a check still running"
+test gate_open_running_check_keeps_it_closed {
+    assert gate_open(3, 1, 0) == false;
+}
+
+// Self-test scenario "two red checks, only one discounted"
+test gate_open_one_blocking_check_keeps_it_closed {
+    assert gate_open(4, 0, 1) == false;
+}
+
+// Tests for run_says, look_back and discount_holds (#7317)
+
+test only_a_completed_failure_says_red {
+    assert run_says(RUN_FAILURE) == MASTER_RED;
+    assert run_says(RUN_SUCCESS) == MASTER_GREEN;
+}
+
+test every_other_run_says_unknown {
+    assert run_says(RUN_MISSING) == MASTER_UNKNOWN;
+    assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN;
+    assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN;
+    assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN;
+    assert run_says(RUN_RUNNING) == MASTER_UNKNOWN;
+    assert run_says(RUN_OTHER) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master newest cancelled, older failure at or after the merge base"
+test look_back_passes_a_cancelled_run_to_an_older_failure {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED;
+}
+
+// Self-test scenario "master failure only before the merge base"
+test look_back_stops_at_the_merge_base {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN;
+    assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN;
+}
+
+// The newest decisive run wins, in both directions
+test look_back_keeps_the_newest_decisive_run {
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN;
+    assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED;
+    assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED;
+}
+
+test look_back_over_only_unknown_runs_stays_unknown {
+    assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN;
+}
+
+// Self-test scenario "master failure": discounted
+test discount_holds_on_a_master_failure {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true;
+}
+
+// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked
+test pr_red_while_master_cancelled_is_blocked {
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false;
+}
+
+// Self-test scenario "master success": the red is the PR's own -> blocked
+test pr_red_while_master_green_is_blocked {
+    assert discount_holds(true, MASTER_GREEN) == false;
+    assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false;
+}
+
+// Master red, but the bee never named the check: blocked
+test master_red_without_the_bees_line_is_blocked {
+    assert discount_holds(false, MASTER_RED) == false;
+    assert discount_holds(false, MASTER_UNKNOWN) == false;
+}
+
+invariant the_run_and_master_codes_never_move {
+    assert RUN_MISSING == 0;
+    assert RUN_FAILURE == 1;
+    assert RUN_SUCCESS == 2;
+    assert RUN_CANCELLED == 3;
+    assert RUN_SKIPPED == 4;
+    assert RUN_TIMED_OUT == 5;
+    assert RUN_RUNNING == 6;
+    assert RUN_OTHER == 7;
+    assert MASTER_UNKNOWN == 0;
+    assert MASTER_RED == 1;
+    assert MASTER_GREEN == 2;
+}
+
+invariant unknown_never_discounts {
+    assert discount_holds(true, MASTER_UNKNOWN) == false;
+    assert discount_holds(true, run_says(RUN_MISSING)) == false;
+    assert discount_holds(true, run_says(RUN_CANCELLED)) == false;
+    assert discount_holds(true, run_says(RUN_SKIPPED)) == false;
+    assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false;
+    assert discount_holds(true, run_says(RUN_RUNNING)) == false;
+    assert discount_holds(true, run_says(RUN_OTHER)) == false;
+    assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-next-key/card.png b/apps/website/public/ru/learn/the-next-key/card.png new file mode 100644 index 0000000000..ace1f32580 Binary files /dev/null and b/apps/website/public/ru/learn/the-next-key/card.png differ diff --git a/apps/website/public/ru/learn/the-next-key/index.html b/apps/website/public/ru/learn/the-next-key/index.html new file mode 100644 index 0000000000..22672f3774 --- /dev/null +++ b/apps/website/public/ru/learn/the-next-key/index.html @@ -0,0 +1,458 @@ + + + + + + +Следующий ключ: курс «Королева и улей», урок 18 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Следующий ключ

+

Вы узнаете

Какой ключ пчела пробует следующим и когда переходит к другому пулу или сдаётся.

+

На 2026-10-17 пчела начала на ключе 9 из 10 и погибла через 7 с, пока 33 других ключа простаивали: она получила один ключ при запуске и держала его. Теперь next_free берёт следующий после последнего ключ, который ещё не пробовали и который не отдыхает. 1305 — перегрузка модели пула, поэтому, когда POOL_OVERLOAD_KEYS, 3, ключа сказали это за минуту, step переводит пчелу в следующий пул. После 8 попыток ход заканчивается.

+

Попробовать

Отметьте ключ 2 как опробованный и посмотрите, какой ключ получит кольцо; затем поставьте три ключа, ответивших 1305.

+

Открыть интерактивный урок →

+
The next key: a bee moves on instead of dying
The next key: a bee moves on instead of dying ↗
+

A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up.

+

specs/queen/key_failover.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993).
+; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them
+; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts
+; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48
+; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54.
+; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider,
+; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no
+; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and
+; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal
+; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not.
+; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM
+; router's cooldown-then-fallback:
+;   1. classify the refusal: transient, quota, auth or a bad request (error_kind);
+;   2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes
+;      it for the long break -- 1302 and 1305 never close a key and never retire work;
+;   3. the next attempt goes to the next free key of the pool, never the key that just failed
+;      (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS
+;      distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next
+;      pool instead of burning the rest (pool_overloaded);
+;   4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708),
+;      its claim is released (trios#1886), and the end is not charged to the issue (charges_issue).
+; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most
+; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides.
+; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenKeyFailover;
+
+; --- 1. What the provider said -----------------------------------------------------------------------
+
+/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may
+/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused.
+/// KE_REQUEST: the request is wrong; every key would answer the same.
+pub const KE_OK : u8 = 0;
+pub const KE_TRANSIENT : u8 = 1;
+pub const KE_QUOTA : u8 = 2;
+pub const KE_AUTH : u8 = 3;
+pub const KE_REQUEST : u8 = 4;
+
+/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own.
+pub const ZAI_RATE_LIMIT : u32 = 1302;
+pub const ZAI_OVERLOAD : u32 = 1305;
+
+/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321
+/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301).
+pub fn is_quota_code(code: u32) -> bool {
+    if (code == 1113) { return true; }
+    if (code == 1312) { return false; }
+    if (code < 1308) { return false; }
+    return code <= 1321;
+}
+
+/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is
+/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai
+/// sends a spent quota and a passing overload with the same 429.
+pub fn error_kind(status: u32, zai_code: u32) -> u8 {
+    if (is_quota_code(zai_code)) { return KE_QUOTA; }
+    if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; }
+    if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; }
+    if (status == 0) { return KE_TRANSIENT; }
+    if (status == 402) { return KE_QUOTA; }
+    if (status == 401) { return KE_AUTH; }
+    if (status == 403) { return KE_AUTH; }
+    if (status == 408) { return KE_TRANSIENT; }
+    if (status == 429) { return KE_TRANSIENT; }
+    if (status >= 600) { return KE_REQUEST; }
+    if (status >= 500) { return KE_TRANSIENT; }
+    if (status >= 400) { return KE_REQUEST; }
+    return KE_OK;
+}
+
+; --- 2. How long a refused key rests ------------------------------------------------------------------
+
+/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than
+/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short
+/// enough that 34 keys are never all resting through a one-minute burst like 14:08.
+pub const COOL_BASE_SECONDS : u32 = 15;
+pub const COOL_CAP_SECONDS : u32 = 240;
+/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes).
+pub const KEY_BREAK_SECONDS : u32 = 1800;
+
+/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled
+/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it.
+pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 {
+    if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; }
+    if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; }
+    if (kind != KE_TRANSIENT) { return 0; }
+    var wait : u32 = COOL_BASE_SECONDS;
+    var n : u32 = 0;
+    while (n < strikes) {
+        if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; }
+        wait = wait * 2;
+        n = n + 1;
+    }
+    return wait;
+}
+
+/// Whether the refusal closes the key for the long break. A transient refusal never does.
+pub fn key_closes(kind: u8) -> bool {
+    if (kind == KE_QUOTA) { return true; }
+    return kind == KE_AUTH;
+}
+
+; --- 3. The next key, the next pool ------------------------------------------------------------------
+
+pub const POOL_MAX : u32 = 32;
+pub const NO_KEY : u32 = 255;
+
+pub fn bit(i: u32) -> u32 {
+    if (i >= POOL_MAX) { return 0; }
+    return 1 << i;
+}
+
+pub fn is_set(mask: u32, i: u32) -> bool {
+    return (mask & bit(i)) != 0;
+}
+
+/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or
+/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of
+/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than
+/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule
+/// picks a key inside a pool and a pool inside the pool list.
+pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 {
+    if (size > POOL_MAX) { return NO_KEY; }
+    var start : u32 = 0;
+    var count : u32 = size;
+    if (last < size) {
+        start = last + 1;
+        count = size - 1;
+    }
+    var k : u32 = 0;
+    while (k < count) {
+        var i : u32 = (start + k) % size;
+        if (is_set(tried | blocked, i) == false) { return i; }
+        k = k + 1;
+    }
+    return NO_KEY;
+}
+
+/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the
+/// pool's model overloaded. A pool smaller than that is overloaded when every key said it.
+pub const POOL_OVERLOAD_KEYS : u32 = 3;
+pub const OVERLOAD_WINDOW_SECONDS : u32 = 60;
+
+pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool {
+    if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; }
+    if (pool_size == 0) { return false; }
+    return distinct_keys_1305 >= pool_size;
+}
+
+; --- 4. The step after a refusal ---------------------------------------------------------------------
+
+/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next
+/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong.
+pub const FA_DONE : u8 = 0;
+pub const FA_NEXT_KEY : u8 = 1;
+pub const FA_NEXT_POOL : u8 = 2;
+pub const FA_GIVE_UP : u8 = 3;
+pub const FA_FAIL : u8 = 4;
+
+/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an
+/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys.
+pub const MAX_ATTEMPTS_PER_TURN : u32 = 8;
+
+/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool`
+/// are next_free's answers for the current pool and for the pool list (NO_KEY when none).
+pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 {
+    if (kind == KE_OK) { return FA_DONE; }
+    if (kind >= KE_REQUEST) { return FA_FAIL; }
+    if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; }
+    var pool_out : bool = false;
+    if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); }
+    if (pool_out == false) {
+        if (next_key != NO_KEY) { return FA_NEXT_KEY; }
+    }
+    if (next_pool != NO_KEY) { return FA_NEXT_POOL; }
+    return FA_GIVE_UP;
+}
+
+/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded,
+/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request
+/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges
+/// every DX_PROVIDER_ERROR.
+pub fn charges_issue(kind: u8) -> bool {
+    return kind == KE_REQUEST;
+}
+
+; --- 5. Tests ----------------------------------------------------------------------------------------
+
+test the_quota_list_is_the_closed_list_of_the_runtime {
+    assert is_quota_code(1113) == true;
+    assert is_quota_code(1308) == true;
+    assert is_quota_code(1311) == true;
+    assert is_quota_code(1312) == false;
+    assert is_quota_code(1313) == true;
+    assert is_quota_code(1321) == true;
+    assert is_quota_code(1322) == false;
+    assert is_quota_code(1307) == false;
+    assert is_quota_code(1112) == false;
+    assert is_quota_code(1114) == false;
+    assert is_quota_code(ZAI_RATE_LIMIT) == false;
+    assert is_quota_code(ZAI_OVERLOAD) == false;
+    assert is_quota_code(0) == false;
+}
+
+test the_code_wins_over_the_status {
+    assert error_kind(429, 1305) == KE_TRANSIENT;
+    assert error_kind(429, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1113) == KE_QUOTA;
+    assert error_kind(429, 1316) == KE_QUOTA;
+    assert error_kind(200, 1308) == KE_QUOTA;
+    assert error_kind(400, 1305) == KE_TRANSIENT;
+    assert error_kind(400, 1302) == KE_TRANSIENT;
+    assert error_kind(429, 1312) == KE_TRANSIENT;
+}
+
+test a_status_without_a_code_falls_in_its_kind {
+    assert error_kind(0, 0) == KE_TRANSIENT;
+    assert error_kind(200, 0) == KE_OK;
+    assert error_kind(399, 0) == KE_OK;
+    assert error_kind(400, 0) == KE_REQUEST;
+    assert error_kind(401, 0) == KE_AUTH;
+    assert error_kind(402, 0) == KE_QUOTA;
+    assert error_kind(403, 0) == KE_AUTH;
+    assert error_kind(404, 0) == KE_REQUEST;
+    assert error_kind(407, 0) == KE_REQUEST;
+    assert error_kind(408, 0) == KE_TRANSIENT;
+    assert error_kind(409, 0) == KE_REQUEST;
+    assert error_kind(428, 0) == KE_REQUEST;
+    assert error_kind(429, 0) == KE_TRANSIENT;
+    assert error_kind(430, 0) == KE_REQUEST;
+    assert error_kind(499, 0) == KE_REQUEST;
+    assert error_kind(500, 0) == KE_TRANSIENT;
+    assert error_kind(503, 0) == KE_TRANSIENT;
+    assert error_kind(529, 0) == KE_TRANSIENT;
+    assert error_kind(599, 0) == KE_TRANSIENT;
+    assert error_kind(600, 0) == KE_REQUEST;
+}
+
+test a_transient_refusal_rests_seconds_and_a_spent_key_closes {
+    assert cooldown_seconds(KE_TRANSIENT, 0) == 15;
+    assert cooldown_seconds(KE_TRANSIENT, 1) == 30;
+    assert cooldown_seconds(KE_TRANSIENT, 2) == 60;
+    assert cooldown_seconds(KE_TRANSIENT, 3) == 120;
+    assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS;
+    assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS;
+    assert cooldown_seconds(KE_OK, 3) == 0;
+    assert cooldown_seconds(KE_REQUEST, 3) == 0;
+    assert key_closes(KE_QUOTA) == true;
+    assert key_closes(KE_AUTH) == true;
+    assert key_closes(KE_TRANSIENT) == false;
+    assert key_closes(KE_OK) == false;
+    assert key_closes(KE_REQUEST) == false;
+}
+
+test bits_name_keys {
+    assert bit(0) == 1;
+    assert bit(3) == 8;
+    assert bit(31) == 2147483648;
+    assert bit(32) == 0;
+    assert is_set(8, 3) == true;
+    assert is_set(8, 2) == false;
+    assert is_set(4294967295, 31) == true;
+    assert is_set(4294967295, 32) == false;
+}
+
+test the_next_key_is_never_the_one_that_failed {
+    // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0
+    assert next_free(bit(9), 0, 10, 9) == 0;
+    // even when the failed key is not marked tried, it is skipped
+    assert next_free(0, 0, 10, 9) == 0;
+    assert next_free(0, 0, 10, 4) == 5;
+    // a pool of one key has no other key
+    assert next_free(0, 0, 1, 0) == NO_KEY;
+    // tried and resting keys are both skipped
+    assert next_free(bit(5), bit(6), 10, 4) == 7;
+    assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY;
+    // a fresh turn starts at key 0
+    assert next_free(0, 0, 24, NO_KEY) == 0;
+    assert next_free(bit(0), 0, 24, NO_KEY) == 1;
+    // every key but the failed one is taken
+    assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY;
+    // an empty pool has nothing
+    assert next_free(0, 0, 0, NO_KEY) == NO_KEY;
+    // `last` just past the end is out of range: the scan starts at 0
+    assert next_free(0, 0, 10, 10) == 0;
+    // a full mask of POOL_MAX keys wraps from the top index
+    assert next_free(0, 0, 32, 31) == 0;
+    assert next_free(0, 0, 32, NO_KEY) == 0;
+    assert next_free(0, 4294967295 - bit(31), 32, 30) == 31;
+    // a list longer than POOL_MAX does not fit a mask and is refused
+    assert next_free(0, 0, 33, NO_KEY) == NO_KEY;
+    assert next_free(0, 0, 40, 31) == NO_KEY;
+}
+
+test an_overload_seen_on_three_keys_is_the_pools_model {
+    assert pool_overloaded(0, 24) == false;
+    assert pool_overloaded(2, 24) == false;
+    assert pool_overloaded(3, 24) == true;
+    assert pool_overloaded(16, 24) == true;
+    assert pool_overloaded(2, 2) == true;
+    assert pool_overloaded(1, 2) == false;
+    assert pool_overloaded(1, 1) == true;
+    assert pool_overloaded(0, 0) == false;
+}
+
+/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that
+/// answered 1305 in the window, pool size, next pool.
+test conformance_rows {
+    // r01 answered: go on
+    assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE;
+    // r02 a wrong request fails, whatever keys are left
+    assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r03 an unknown kind is read as a wrong request
+    assert step(9, 1, 5, 0, 24, 1) == FA_FAIL;
+    // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key
+    assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY;
+    // r05 a rate limit (1302) on a fresh pool: next key
+    assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY;
+    // r06 the third distinct key said 1305: the model is overloaded, next pool
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL;
+    // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest
+    assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP;
+    // r08 a spent quota on one key does not mean an overloaded pool: next key
+    assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY;
+    // r09 a refused key: next key
+    assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY;
+    // r10 no key left in the pool: next pool
+    assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL;
+    // r11 no key and no pool left: give up
+    assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP;
+    // r12 the attempt bound stops a turn with keys still left
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP;
+    // r13 one attempt below the bound still moves on
+    assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY;
+    // r14 a two-key pool where both said 1305 is overloaded
+    assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL;
+    // r15 a 503 with no 1305 seen keeps to the pool
+    assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY;
+    // r16 the bound beats a wrong request? no: a wrong request fails first
+    assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL;
+    // r17 an answer after many attempts is still an answer
+    assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE;
+}
+
+test an_overloaded_provider_never_retires_the_issue {
+    assert charges_issue(KE_TRANSIENT) == false;
+    assert charges_issue(KE_QUOTA) == false;
+    assert charges_issue(KE_AUTH) == false;
+    assert charges_issue(KE_REQUEST) == true;
+    assert charges_issue(KE_OK) == false;
+    assert charges_issue(9) == false;
+}
+
+; --- 6. Invariants -----------------------------------------------------------------------------------
+
+invariant the_kinds_and_steps_are_distinct {
+    assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4;
+    assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4;
+}
+
+/// A transient rest is always shorter than the long break, so a 1305 can never close a key.
+invariant a_rest_is_shorter_than_a_break {
+    assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16;
+    assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800;
+}
+
+/// The bound leaves room to see an overload in one pool and still try the next: the overload is
+/// detected before the bound stops the turn.
+invariant the_bound_leaves_room_for_a_second_pool {
+    assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2;
+    assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0;
+}
+
+/// NO_KEY is no index a mask can hold.
+invariant no_key_is_out_of_range {
+    assert POOL_MAX == 32 and NO_KEY >= POOL_MAX;
+    assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-night-watch/card.png b/apps/website/public/ru/learn/the-night-watch/card.png new file mode 100644 index 0000000000..a09bf1a847 Binary files /dev/null and b/apps/website/public/ru/learn/the-night-watch/card.png differ diff --git a/apps/website/public/ru/learn/the-night-watch/index.html b/apps/website/public/ru/learn/the-night-watch/index.html new file mode 100644 index 0000000000..79f118987a --- /dev/null +++ b/apps/website/public/ru/learn/the-night-watch/index.html @@ -0,0 +1,220 @@ + + + + + + +Ночной дозор: курс «Королева и улей», урок 25 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Ночной дозор

+

Вы узнаете

Что ночной дозор делает утром с четырьмя проверками карточек.

+

Каждую ночь идут четыре проверки чата карточек: тред, дверь статуса, слияния и паритет. night_verdict подводит итог: любое падение — это падение, иначе любое неизмеренное — неизмерено, иначе всё прошло. night_report превращает это в одно действие: падение открывает задачу или комментирует открытую, успех закрывает открытую задачу или молчит, а неизмеренная ночь оставляет всё как есть.

+

Попробовать

Сделайте ночь успешной при открытой задаче; затем сделайте одну проверку неизмеренной.

+

Открыть интерактивный урок →

+
The night watch: four checks, one verdict, one report
The night watch: four checks, one verdict, one report ↗
+

Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent.

+

specs/automation/card-chat-night-watch.t27

// SPDX-License-Identifier: Apache-2.0
+// Card Chat Night Watch - verdict and reporting logic
+// Part of EPIC #5388
+
+module CardChatNightWatch {
+    // Verdict constants
+    const CHECK_PASS: u8 = 0
+    const CHECK_FAIL: u8 = 1
+    const CHECK_UNMEASURED: u8 = 2
+
+    // Report constants
+    const REPORT_SILENT: u8 = 0
+    const REPORT_OPEN_ISSUE: u8 = 1
+    const REPORT_COMMENT: u8 = 2
+    const REPORT_CLOSE_ISSUE: u8 = 3
+    const REPORT_LEAVE_AS_IS: u8 = 4
+
+    // Total number of card check verdicts (thread, status door, merges, parity)
+    const CARD_CHECK_VERDICTS: u8 = 4
+
+    // Determine night verdict based on failures and unmeasured checks
+    // Any fail -> FAIL; else any unmeasured -> UNMEASURED; else PASS
+    fn night_verdict(fails: u8, unmeasured: u8) -> u8 {
+        if fails > 0 {
+            return CHECK_FAIL
+        } else if unmeasured > 0 {
+            return CHECK_UNMEASURED
+        } else {
+            return CHECK_PASS
+        }
+    }
+
+    // Determine night report based on verdict and issue state
+    // FAIL and no open issue -> OPEN_ISSUE
+    // FAIL and an open issue -> COMMENT
+    // PASS and an open issue -> CLOSE_ISSUE
+    // PASS and none -> SILENT
+    // UNMEASURED -> LEAVE_AS_IS whatever the issue state
+    // a verdict above 2 -> LEAVE_AS_IS
+    fn night_report(verdict: u8, issue_open: bool) -> u8 {
+        if verdict > 2 {
+            return REPORT_LEAVE_AS_IS
+        }
+        
+        if verdict == CHECK_FAIL {
+            if issue_open {
+                return REPORT_COMMENT
+            } else {
+                return REPORT_OPEN_ISSUE
+            }
+        } else if verdict == CHECK_PASS {
+            if issue_open {
+                return REPORT_CLOSE_ISSUE
+            } else {
+                return REPORT_SILENT
+            }
+        } else {
+            return REPORT_LEAVE_AS_IS
+        }
+    }
+
+    // Check if night counts are acceptable
+    // true when fails + unmeasured <= CARD_CHECK_VERDICTS
+    fn night_counts_ok(fails: u8, unmeasured: u8) -> bool {
+        let total = fails + unmeasured
+        return total <= CARD_CHECK_VERDICTS
+    }
+
+    // Tests
+    test night_verdict_all_pass
+        let result = night_verdict(0, 0)
+        assert result == CHECK_PASS
+
+    test night_verdict_any_fail
+        let result = night_verdict(1, 0)
+        assert result == CHECK_FAIL
+
+    test night_verdict_fail_beside_unmeasured
+        let result = night_verdict(1, 3)
+        assert result == CHECK_FAIL
+
+    test night_verdict_any_unmeasured_no_fail
+        let result = night_verdict(0, 1)
+        assert result == CHECK_UNMEASURED
+
+    test night_report_fail_no_issue
+        let result = night_report(CHECK_FAIL, false)
+        assert result == REPORT_OPEN_ISSUE
+
+    test night_report_fail_with_issue
+        let result = night_report(CHECK_FAIL, true)
+        assert result == REPORT_COMMENT
+
+    test night_report_pass_with_issue
+        let result = night_report(CHECK_PASS, true)
+        assert result == REPORT_CLOSE_ISSUE
+
+    test night_report_pass_no_issue
+        let result = night_report(CHECK_PASS, false)
+        assert result == REPORT_SILENT
+
+    test night_report_unmeasured_with_issue
+        let result = night_report(CHECK_UNMEASURED, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_report_unmeasured_no_issue
+        let result = night_report(CHECK_UNMEASURED, false)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_report_invalid_verdict
+        let result = night_report(5, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test night_counts_ok_within_limit
+        let result = night_counts_ok(2, 2)
+        assert result == true
+
+    test night_counts_ok_at_limit
+        let result = night_counts_ok(4, 0)
+        assert result == true
+
+    test night_counts_ok_over_limit
+        let result = night_counts_ok(3, 2)
+        assert result == false
+
+    test night_counts_ok_zero
+        let result = night_counts_ok(0, 0)
+        assert result == true
+
+    // User scenario tests
+    test scenario_unmeasured_night_with_open_issue
+        // Given a night where the render answered 401 to every read
+        // when night_report(night_verdict(0, 4), true) is evaluated
+        // then it returns REPORT_LEAVE_AS_IS
+        let verdict = night_verdict(0, 4)
+        let result = night_report(verdict, true)
+        assert result == REPORT_LEAVE_AS_IS
+
+    test scenario_fail_night_no_open_issue
+        // Given a night where the parity check failed and nothing else did
+        // when night_report(night_verdict(1, 0), false) is evaluated
+        // then it returns REPORT_OPEN_ISSUE
+        let verdict = night_verdict(1, 0)
+        let result = night_report(verdict, false)
+        assert result == REPORT_OPEN_ISSUE
+}

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-public-board/card.png b/apps/website/public/ru/learn/the-public-board/card.png new file mode 100644 index 0000000000..6c7e05cef1 Binary files /dev/null and b/apps/website/public/ru/learn/the-public-board/card.png differ diff --git a/apps/website/public/ru/learn/the-public-board/index.html b/apps/website/public/ru/learn/the-public-board/index.html new file mode 100644 index 0000000000..d578ff6596 --- /dev/null +++ b/apps/website/public/ru/learn/the-public-board/index.html @@ -0,0 +1,669 @@ + + + + + + +Публичная доска: курс «Королева и улей», урок 26 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Публичная доска

+

Вы узнаете

В какой колонке публичная доска рисует каждое притязание и в каком случае ошибается.

+

Доска на app.t27.ai/game/kanban рисует каждый запуск в одной из 6 колонок. column_of читает притязание запуска на его задачу: running — в работе, accepted — готово, rejected и awaiting review — на ревью, failed — сброшено. dispatch.t27 записывает одну находку как есть: когда задача закрыта и список прочитан, любое завершённое притязание рисуется как готовое, каким бы ни был вердикт.

+

Попробовать

Откройте задачу и посмотрите, куда уйдут четыре завершённых притязания; затем скажите, что скрывает закрытая задача.

+

Открыть интерактивный урок →

+
The public board: which column a task's card is in
The public board: which column a task's card is in ↗
+

Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict.

+

specs/queen/dispatch.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done
+; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written
+; down in this family of repositories and only one of them moves work: the round of the trios supervisor
+; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it,
+; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with
+; the other cycles named as adapters and their gaps stated. Every decision below is a function the C
+; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py):
+;   ts      its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the
+;           public board's column, the single-flight round gate
+;   queend  its Swift policy binary, built at the pin and asked choose, review and retry
+;   pg      PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch
+;           writers on real rows, the criteria release, the CI take-back, the round's own SELECT
+;           (17.7 at this pin; the run at the old pin c25e1b02 used 16.15)
+;   lotus   gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which
+;           this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin
+;           c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host)
+;   live    one snapshot of the public endpoints of the deployed supervisor
+; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch
+; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read
+; from the source and named below: runner lanes (a claim in state queued that takes no worker slot,
+; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees)
+; and the public card's verdict and judgedHead.
+; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a
+; revision other than the pin. ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenDispatch;
+
+pub const KIND : str = "queen-dispatch";
+pub const ID : str = "queen/dispatch";
+pub const NAME : str = "The Queen's task lifecycle, from candidate to done";
+pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS";
+pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size";
+pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef";
+pub const CONSUMER_REPO : str = "gHashTag/trinity";
+pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
+pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530";
+pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149";
+; --- The decision: one canonical machine, the others are adapters ---------------------------------
+pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)";
+pub const ADAPTERS : [5]str = [
+    "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend",
+    "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design",
+    "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing",
+    "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task",
+    "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here"
+];
+pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state";
+pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"];
+; --- States of a dispatch row, and the events that move it ----------------------------------------
+pub const S_NONE : u32 = 0;
+pub const S_RUNNING : u32 = 1;
+pub const S_REFUSED : u32 = 2;
+pub const S_ENDED : u32 = 3;
+pub const S_WAIT : u32 = 4;
+pub const S_ACCEPT : u32 = 5;
+pub const S_SENDBACK : u32 = 6;
+pub const S_ESCALATE : u32 = 7;
+pub const S_EMPTY : u32 = 8;
+pub const S_STALE : u32 = 9;
+pub const S_REAPED : u32 = 10;
+pub const S_RELEASED : u32 = 11;
+pub const ILLEGAL : u32 = 99;
+pub const E_START : u32 = 0;
+pub const E_REFUSE : u32 = 1;
+pub const E_END : u32 = 2;
+pub const E_END_OTHER_TURN : u32 = 3;
+pub const E_REAP_STALL : u32 = 4;
+pub const E_REAP_BOOT : u32 = 5;
+pub const E_VERDICT_ACCEPT : u32 = 6;
+pub const E_VERDICT_SENDBACK : u32 = 7;
+pub const E_VERDICT_ESCALATE : u32 = 8;
+pub const E_VERDICT_WAIT : u32 = 9;
+pub const E_VERDICT_EMPTY : u32 = 10;
+pub const E_CRITERIA_CHANGED : u32 = 11;
+pub const E_CI_RED_UNDER_CEILING : u32 = 12;
+pub const E_CI_RED_AT_CEILING : u32 = 13;
+pub const E_FLOOR_PASSED : u32 = 14;
+pub const E_REDISPATCH : u32 = 15;
+pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)";
+pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation";
+pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it";
+; --- Claims: what a row costs its issue ------------------------------------------------------------
+pub const V_NONE : u32 = 0;
+pub const V_BLANK : u32 = 1;
+pub const V_WAIT : u32 = 2;
+pub const V_EMPTY : u32 = 3;
+pub const V_SENDBACK : u32 = 4;
+pub const V_ESCALATE : u32 = 5;
+pub const V_ACCEPT : u32 = 6;
+pub const V_FAILED : u32 = 7;
+pub const V_CANCELLED : u32 = 8;
+pub const V_STALE : u32 = 9;
+pub const V_OTHER : u32 = 10;
+pub const C_RUNNING : u32 = 0;
+pub const C_ACCEPTED : u32 = 1;
+pub const C_REJECTED : u32 = 2;
+pub const C_AWAITING_REVIEW : u32 = 3;
+pub const C_FAILED : u32 = 4;
+pub const EMPTY_FLOOR_MS : u64 = 1800000;
+pub const SENDBACK_FLOOR_MS : u64 = 3600000;
+pub const WAIT_FLOOR_MS : u64 = 21600000;
+pub const CEILING_RELEASE_MS : u64 = 3600000;
+pub const MAX_CEILING_RELEASES : u32 = 1;
+pub const SENDBACK_CEILING : u32 = 2;
+pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it";
+; --- Choosing ----------------------------------------------------------------------------------------
+pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates";
+pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies";
+pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing";
+pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only";
+pub const DEFAULT_WORKER_LIMIT : u32 = 4;
+pub const NONE_CHOSEN : u32 = 5;
+pub const COL_BACKLOG : u32 = 0;
+pub const COL_BLOCKED : u32 = 1;
+pub const COL_RUNNING : u32 = 2;
+pub const COL_REVIEW : u32 = 3;
+pub const COL_DONE : u32 = 4;
+pub const COL_DROPPED : u32 = 5;
+; --- Leases ------------------------------------------------------------------------------------------
+pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced";
+pub const LEASE_TTL_SECONDS : u32 = 180;
+pub const LEASE_HEARTBEAT_SECONDS : u32 = 60;
+pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane";
+pub const STALL_MINUTES : u32 = 120;
+; --- Review, retries, evidence, cancellation --------------------------------------------------------
+pub const R_ACCEPT : u32 = 0;
+pub const R_SENDBACK : u32 = 1;
+pub const R_ESCALATE : u32 = 2;
+pub const R_WAIT : u32 = 3;
+pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)";
+pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container";
+pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists";
+pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict";
+pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped";
+; --- Events, persistence, idempotency, projections --------------------------------------------------
+pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)";
+pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round";
+pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"];
+pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board";
+
+; --- End reasons for runs that produce no commits -----------------------------------------------------------
+; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted.
+; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days.
+pub const R_MODEL_ENDED_TURN : u32 = 0;      // Model ended its turn without editing any files
+pub const R_TIME_TOKEN_LIMIT : u32 = 1;     // Hit time or token limit during execution
+pub const R_CRASHED : u32 = 2;             // Container or tool crashed (with exit code)
+pub const R_BOUNDARY_CORRECT : u32 = 3;     // Boundary file was already correct, no changes needed
+pub const R_REFUSAL_START : u32 = 4;       // Refusal before the run started (invalid task, missing resources)
+pub const R_PUSH_FAILED : u32 = 5;          // Push to remote failed after successful run
+pub const R_UNKNOWN : u32 = 6;             // Cause could not be determined by the runner
+
+pub const END_REASON_COUNT : u32 = 7;
+pub const END_REASON_NAMES : [7]str = [
+    "model_ended_turn",
+    "time_token_limit", 
+    "crashed",
+    "boundary_correct",
+    "refusal_start",
+    "push_failed",
+    "unknown"
+];
+
+; --- Public board extension: show end reason on empty cards -----------------------------------------------
+; The public board's "empty" cards now include the end reason when available, enabling breakdown of
+; the 56.0% empty-branch share by reason from public endpoints alone.
+pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available";
+pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints";
+; --- Agents and workers ---------------------------------------------------------------------------------
+pub const AGENT_LETTERS : u32 = 27;
+pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27";
+; --- The specs this one sits beside ----------------------------------------------------------------------
+pub const NEIGHBOURS : [6]str = [
+    "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master",
+    "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort",
+    "specs/queen/brain_summaries.t27: does not compile in C at master",
+    "specs/brain/brain.t27: no function; does not compile in C at master",
+    "specs/brain/bus.t27: compiles, one test of a version constant",
+    "specs/brain/cognitive_loop.t27: compiles, one test of a version constant"
+];
+; --- Findings: measured, the tag names the evidence ------------------------------------------------------
+pub const FINDINGS_COUNT : u32 = 15;
+pub const FINDINGS : [15]str = [
+    "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]",
+    "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]",
+    "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]",
+    "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]",
+    "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]",
+    "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]",
+    "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]",
+    "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]",
+    "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]",
+    "f59 nothing cancels a running bee [source]",
+    "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]",
+    "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]",
+    "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]",
+    "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]",
+    "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]"
+];
+pub const ENABLED : bool = true;
+
+; --- Rules ------------------------------------------------------------------------------------------------------
+
+pub fn next_state(state: u32, event: u32) -> u32 {
+    if (state == S_NONE) {
+        if (event == E_START) { return S_RUNNING; }
+        if (event == E_REFUSE) { return S_REFUSED; }
+        return ILLEGAL;
+    }
+    if (state == S_RUNNING) {
+        if (event == E_END) { return S_ENDED; }
+        if (event == E_REAP_STALL) { return S_REAPED; }
+        if (event == E_REAP_BOOT) { return S_REAPED; }
+        return ILLEGAL;
+    }
+    if (state == S_ENDED or state == S_WAIT) {
+        if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; }
+        if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; }
+        if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; }
+        if (event == E_VERDICT_WAIT) { return S_WAIT; }
+        if (event == E_VERDICT_EMPTY) { return S_EMPTY; }
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ACCEPT) {
+        if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; }
+        if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; }
+        return ILLEGAL;
+    }
+    if (state == S_SENDBACK or state == S_EMPTY) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        if (event == E_FLOOR_PASSED) { return S_RELEASED; }
+        return ILLEGAL;
+    }
+    if (state == S_ESCALATE) {
+        if (event == E_CRITERIA_CHANGED) { return S_STALE; }
+        return ILLEGAL;
+    }
+    if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) {
+        if (event == E_REDISPATCH) { return S_RUNNING; }
+        return ILLEGAL;
+    }
+    return ILLEGAL;
+}
+
+pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 {
+    if (finished == false) { return C_RUNNING; }
+    if (verdict == V_ACCEPT) { return C_ACCEPTED; }
+    if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; }
+    if (verdict == V_EMPTY) {
+        if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_STALE) { return C_FAILED; }
+    if (verdict == V_SENDBACK) {
+        if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+        if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; }
+        if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; }
+        return C_REJECTED;
+    }
+    if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) {
+        if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; }
+    }
+    return C_AWAITING_REVIEW;
+}
+
+; What the round and the board actually pass for `releases`: neither selects the column.
+pub fn releases_read_by_round(stored: u32) -> u32 {
+    return 0;
+}
+
+pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 {
+    if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; }
+    if (claim == C_RUNNING) { return COL_RUNNING; }
+    if (claim == C_ACCEPTED) { return COL_DONE; }
+    if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; }
+    return COL_DROPPED;
+}
+
+pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 {
+    if (e0) { return 0; }
+    if (e1) { return 1; }
+    if (e2) { return 2; }
+    if (e3) { return 3; }
+    if (e4) { return 4; }
+    return NONE_CHOSEN;
+}
+
+pub fn can_start(running: u32, limit: u32) -> bool {
+    return running < limit;
+}
+
+pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 {
+    if (total == 0) { return R_ESCALATE; }
+    if (judged < total) { return R_WAIT; }
+    if (met == total) {
+        if (committed_known and committed > 0) { return R_ACCEPT; }
+        return R_ESCALATE;
+    }
+    if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; }
+    return R_SENDBACK;
+}
+
+pub fn retry_allowed(real_attempts: u32) -> bool {
+    return real_attempts < SENDBACK_CEILING;
+}
+
+pub fn counts_against_issue(interrupted: bool) -> bool {
+    return interrupted == false;
+}
+
+pub fn lease_taken(expired: bool, same_holder: bool) -> bool {
+    return expired or same_holder;
+}
+
+pub fn fence_after(fence: u64) -> u64 {
+    return fence + 1;
+}
+
+pub fn owners_per_term() -> u32 {
+    return 1;
+}
+
+pub fn stall_reapable(age_minutes: u32) -> bool {
+    return age_minutes > STALL_MINUTES;
+}
+
+pub fn followup_rounds(requests_during_round: u32) -> u32 {
+    if (requests_during_round > 0) { return 1; }
+    return 0;
+}
+
+pub fn accept_requires_pull_request() -> bool {
+    return false;
+}
+
+pub fn writer_enforces_table() -> bool {
+    return false;
+}
+
+invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; }
+invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; }
+invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; }
+invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; }
+invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); }
+invariant all_end_reasons_valid { 
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN);
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT);
+    assert is_valid_end_reason(R_CRASHED);
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT);
+    assert is_valid_end_reason(R_REFUSAL_START);
+    assert is_valid_end_reason(R_PUSH_FAILED);
+    assert is_valid_end_reason(R_UNKNOWN);
+}
+
+; --- End reason handling functions --------------------------------------------------------------------------
+
+pub fn is_valid_end_reason(reason: u32) -> bool {
+    return reason < END_REASON_COUNT;
+}
+
+pub fn end_reason_to_name(reason: u32) -> str {
+    if (is_valid_end_reason(reason)) {
+        return END_REASON_NAMES[reason];
+    }
+    return "invalid_end_reason";
+}
+
+pub fn is_no_diff_end_reason(reason: u32) -> bool {
+    return reason == R_MODEL_ENDED_TURN 
+        or reason == R_TIME_TOKEN_LIMIT 
+        or reason == R_BOUNDARY_CORRECT 
+        or reason == R_REFUSAL_START 
+        or reason == R_PUSH_FAILED 
+        or reason == R_UNKNOWN;
+}
+
+pub fn is_crash_end_reason(reason: u32) -> bool {
+    return reason == R_CRASHED;
+}
+
+test the_table_moves_only_along_its_guards {
+    assert next_state(S_NONE, E_START) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END) == S_ENDED;
+    assert next_state(S_ENDED, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE;
+    assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL;
+    assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL;
+    assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL;
+    // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886).
+    assert next_state(S_NONE, E_REFUSE) == S_REFUSED;
+    assert next_state(S_NONE, E_END) == ILLEGAL;
+    assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED;
+    assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED;
+    assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK;
+    assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE;
+    assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT;
+    assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY;
+    assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT;
+    assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK;
+    assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED;
+    assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_SENDBACK, E_START) == ILLEGAL;
+    assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE;
+    assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING;
+    assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING;
+}
+
+test a_claim_follows_the_verdict_and_three_clocks {
+    assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING;
+    assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED;
+    assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED;
+    assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED;
+    assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW;
+    // #9886: each verdict that frees the issue, the send-back floor at its edge with a release
+    // already spent, and the wait floor's two edges.
+    assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED;
+    assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED;
+    assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED;
+    assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW;
+    assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW;
+}
+
+test a_closed_issue_is_drawn_done_whatever_its_verdict {
+    assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE;
+    assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW;
+    assert column_of(C_RUNNING, false, true) == COL_RUNNING;
+    assert column_of(C_FAILED, true, true) == COL_DROPPED;
+    assert column_of(C_ACCEPTED, true, true) == COL_DONE;
+}
+
+test the_first_eligible_wins_in_any_order {
+    assert chosen_of(false, false, true, true, false) == 2;
+    assert chosen_of(false, true, false, false, true) == 1;
+    assert chosen_of(false, false, false, false, false) == NONE_CHOSEN;
+    assert chosen_of(true, true, false, false, false) == 0;
+    assert chosen_of(false, false, false, true, true) == 3;
+    assert chosen_of(false, false, false, false, true) == 4;
+    assert can_start(3, DEFAULT_WORKER_LIMIT) == true;
+    assert can_start(4, DEFAULT_WORKER_LIMIT) == false;
+}
+
+test review_escalates_without_work_and_after_two_send_backs {
+    assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE;
+    assert review_of(2, 1, 1, true, 1, 0) == R_WAIT;
+    assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT;
+    assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE;
+    assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK;
+    assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE;
+    assert retry_allowed(1) == true;
+    assert retry_allowed(2) == false;
+    assert counts_against_issue(true) == false;
+    assert counts_against_issue(false) == true;
+}
+
+test one_lease_one_owner_and_age_alone_reaps {
+    assert lease_taken(false, false) == false;
+    assert lease_taken(true, false) == true;
+    assert lease_taken(false, true) == true;
+    assert fence_after(3) == 4;
+    assert owners_per_term() == 1;
+    assert stall_reapable(121) == true;
+    assert stall_reapable(119) == false;
+    // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself.
+    assert stall_reapable(120) == false;
+    assert followup_rounds(5) == 1;
+    assert followup_rounds(0) == 0;
+    assert accept_requires_pull_request() == false;
+    assert writer_enforces_table() == false;
+}
+
+; --- End reason tests --------------------------------------------------------------------------------------
+
+test end_reason_constants_are_correct {
+    assert R_MODEL_ENDED_TURN == 0;
+    assert R_TIME_TOKEN_LIMIT == 1;
+    assert R_CRASHED == 2;
+    assert R_BOUNDARY_CORRECT == 3;
+    assert R_REFUSAL_START == 4;
+    assert R_PUSH_FAILED == 5;
+    assert R_UNKNOWN == 6;
+    assert END_REASON_COUNT == 7;
+    // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two
+    // call the functions that read the constants: the last reason is named, one past it is not.
+    assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown";
+    assert is_valid_end_reason(END_REASON_COUNT) == false;
+}
+
+test end_reason_names_array_matches_constants {
+    assert END_REASON_NAMES[0] == "model_ended_turn";
+    assert END_REASON_NAMES[1] == "time_token_limit";
+    assert END_REASON_NAMES[2] == "crashed";
+    assert END_REASON_NAMES[3] == "boundary_correct";
+    assert END_REASON_NAMES[4] == "refusal_start";
+    assert END_REASON_NAMES[5] == "push_failed";
+    assert END_REASON_NAMES[6] == "unknown";
+}
+
+test end_reason_validation_works {
+    assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_valid_end_reason(R_CRASHED) == true;
+    assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_valid_end_reason(R_REFUSAL_START) == true;
+    assert is_valid_end_reason(R_PUSH_FAILED) == true;
+    assert is_valid_end_reason(R_UNKNOWN) == true;
+    assert is_valid_end_reason(7) == false;  // One past the last valid reason
+    assert is_valid_end_reason(99) == false; // Way past the last valid reason
+}
+
+test end_reason_to_name_conversion {
+    assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn";
+    assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit";
+    assert end_reason_to_name(R_CRASHED) == "crashed";
+    assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct";
+    assert end_reason_to_name(R_REFUSAL_START) == "refusal_start";
+    assert end_reason_to_name(R_PUSH_FAILED) == "push_failed";
+    assert end_reason_to_name(R_UNKNOWN) == "unknown";
+    assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string
+}
+
+test no_diff_end_reason_classification {
+    assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true;
+    assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true;
+    assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true;
+    assert is_no_diff_end_reason(R_REFUSAL_START) == true;
+    assert is_no_diff_end_reason(R_PUSH_FAILED) == true;
+    assert is_no_diff_end_reason(R_UNKNOWN) == true;
+    assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category
+}
+
+test crash_end_reason_classification {
+    assert is_crash_end_reason(R_CRASHED) == true;
+    assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false;
+    assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false;
+    assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false;
+    assert is_crash_end_reason(R_REFUSAL_START) == false;
+    assert is_crash_end_reason(R_PUSH_FAILED) == false;
+    assert is_crash_end_reason(R_UNKNOWN) == false;
+}
+
+test end_reason_enables_breakdown_of_empty_branches {
+    // This test validates that the end reason vocabulary enables breaking down
+    // the 56.0% empty-branch share by reason from public endpoints alone
+    
+    // Simulate counts for empty branches by reason
+    var model_ended_count : u32 = 100;
+    var token_limit_count : u32 = 80;
+    var boundary_correct_count : u32 = 60;
+    var refusal_start_count : u32 = 40;
+    var push_failed_count : u32 = 30;
+    var unknown_count : u32 = 7;  // Should be < 10% of total
+    var crash_count : u32 = 0;
+    
+    var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                           refusal_start_count + push_failed_count + unknown_count + crash_count;
+    
+    // Verify unknown is less than 10% of total (success criterion from issue)
+    assert unknown_count < total_empty / 10;
+    
+    // Verify we can calculate breakdown percentages
+    var model_percentage : u32 = (model_ended_count * 100) / total_empty;
+    var token_percentage : u32 = (token_limit_count * 100) / total_empty;
+    var unknown_percentage : u32 = (unknown_count * 100) / total_empty;
+    
+    assert model_percentage > 0;
+    assert token_percentage > 0;
+    assert unknown_percentage < 10;  // Unknown should be < 10%
+    
+    // Verify no-diff vs crash categorization works
+    var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + 
+                              refusal_start_count + push_failed_count + unknown_count;
+    var crash_total : u32 = crash_count;
+    
+    assert no_diff_total == total_empty - crash_total;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-queens-move/card.png b/apps/website/public/ru/learn/the-queens-move/card.png new file mode 100644 index 0000000000..281484d7a5 Binary files /dev/null and b/apps/website/public/ru/learn/the-queens-move/card.png differ diff --git a/apps/website/public/ru/learn/the-queens-move/index.html b/apps/website/public/ru/learn/the-queens-move/index.html new file mode 100644 index 0000000000..361b50443e --- /dev/null +++ b/apps/website/public/ru/learn/the-queens-move/index.html @@ -0,0 +1,349 @@ + + + + + + +Ход Королевы: курс «Королева и улей», урок 14 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Ход Королевы

+

Вы узнаете

Что Королева делает в каждом заторе и каких заторов она ждёт.

+

У каждого случая одно действие на той же позиции: no reviewer отправляет ревьюера, refused not fixed отправляет кодера, refused then pushed отправляет повторное ревью, а labeled not merged завершает слияние. jam_action переводит любое значение больше 9 в measure again, так что неизвестный случай никогда не значит «делать нечего». jam_is_queens_turn ложно для 3 случаев — none, draft or fresh и stacked on an open PR: их она ждёт.

+

Попробовать

Найдите случай, чьё действие — ждать базу; затем дотяните до 200 и прочитайте действие.

+

Открыть интерактивный урок →

+
The Queen's move: what each jam asks of her
The Queen's move: what each jam asks of her ↗
+

Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting.

+

specs/automation/review-jam.t27

// Review jam: which case a stuck pull request is in, and the Queen's action.
+// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700).
+// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead
+// of hand-copying them.
+
+module review_jam;
+
+// Jam case constants (u8, value = position, starting at 0)
+pub const JAM_NONE: u8 = 0;
+pub const JAM_NO_REVIEWER: u8 = 1;
+pub const JAM_STACKED_ON_OPEN: u8 = 2;
+pub const JAM_REFUSED_NOT_FIXED: u8 = 3;
+pub const JAM_REFUSED_THEN_PUSHED: u8 = 4;
+pub const JAM_LABELED_NOT_MERGED: u8 = 5;
+pub const JAM_CONFLICTING: u8 = 6;
+pub const JAM_NOISE_RED: u8 = 7;
+pub const JAM_DRAFT_OR_FRESH: u8 = 8;
+pub const JAM_UNMEASURED: u8 = 9;
+
+// Queen action constants (u8, value = position, starting at 0)
+pub const ACT_NOTHING: u8 = 0;
+pub const ACT_DISPATCH_REVIEWER: u8 = 1;
+pub const ACT_WAIT_FOR_BASE: u8 = 2;
+pub const ACT_DISPATCH_CODER: u8 = 3;
+pub const ACT_DISPATCH_RE_REVIEW: u8 = 4;
+pub const ACT_FINISH_MERGE: u8 = 5;
+pub const ACT_REBASE_BY_CODER: u8 = 6;
+pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7;
+pub const ACT_WAIT: u8 = 8;
+pub const ACT_MEASURE_AGAIN: u8 = 9;
+
+// The jam case of a pull request. The FIRST matching rule wins:
+//  1. not measured                      -> UNMEASURED
+//  2. draft or fresh                    -> DRAFT_OR_FRESH
+//  3. stacked on an open PR             -> STACKED_ON_OPEN
+//  4. conflicting                       -> CONFLICTING
+//  5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
+//  6. refused and pushed after          -> REFUSED_THEN_PUSHED
+//  7. refused                           -> REFUSED_NOT_FIXED
+//  8. only noise checks red             -> NOISE_RED
+//  9. no reviewer                       -> NO_REVIEWER
+// 10. otherwise                         -> NONE
+pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 {
+    if !measured {
+        return JAM_UNMEASURED;
+    }
+    if draft_or_fresh {
+        return JAM_DRAFT_OR_FRESH;
+    }
+    if stacked_on_open {
+        return JAM_STACKED_ON_OPEN;
+    }
+    if conflicting {
+        return JAM_CONFLICTING;
+    }
+    if labeled_reviewed {
+        return JAM_LABELED_NOT_MERGED;
+    }
+    if refused && pushed_after_refusal {
+        return JAM_REFUSED_THEN_PUSHED;
+    }
+    if refused {
+        return JAM_REFUSED_NOT_FIXED;
+    }
+    if only_noise_red {
+        return JAM_NOISE_RED;
+    }
+    if !has_reviewer {
+        return JAM_NO_REVIEWER;
+    }
+    JAM_NONE
+}
+
+// The Queen's action for a case: the action with the SAME position.
+// Any value above 9 is an unknown case, and an unknown case is never
+// "nothing to do": it maps to ACT_MEASURE_AGAIN.
+pub fn jam_action(case: u8) -> u8 {
+    if case > 9 {
+        return ACT_MEASURE_AGAIN;
+    }
+    case
+}
+
+// Whether the case is the Queen's turn to act: true for every case except
+// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN.
+pub fn jam_is_queens_turn(case: u8) -> bool {
+    case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN
+}
+
+// ---- jam_case: one test per case ----
+
+test "unmeasured beats every other flag" {
+    assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
+}
+
+test "unmeasured with every other flag clear" {
+    assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED;
+}
+
+test "draft or fresh" {
+    assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked on open PR" {
+    assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting" {
+    assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled not merged" {
+    assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed" {
+    assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed" {
+    assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red" {
+    assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED;
+}
+
+test "no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER;
+}
+
+test "none" {
+    assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE;
+}
+
+// ---- jam_case: first-match order, each rule against the next one ----
+
+test "draft beats stacked" {
+    assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked and unreviewed is stacked, not no reviewer" {
+    assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "stacked beats conflicting" {
+    assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting beats labeled" {
+    assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled beats refused then pushed" {
+    assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red beats no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
+}
+
+test "pushed without a refusal is not refused then pushed" {
+    assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE;
+}
+
+// ---- jam_action: same position, unknown -> measure again ----
+
+test "action none to nothing" {
+    assert jam_action(JAM_NONE) == ACT_NOTHING;
+}
+
+test "action no reviewer to dispatch reviewer" {
+    assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
+}
+
+test "action stacked to wait for base" {
+    assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE;
+}
+
+test "action refused not fixed to dispatch coder" {
+    assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER;
+}
+
+test "action refused then pushed to dispatch re-review" {
+    assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW;
+}
+
+test "action labeled not merged to finish merge" {
+    assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE;
+}
+
+test "action conflicting to rebase by coder" {
+    assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER;
+}
+
+test "action noise red to name noise and review" {
+    assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW;
+}
+
+test "action draft or fresh to wait" {
+    assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT;
+}
+
+test "action unmeasured to measure again" {
+    assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN;
+}
+
+test "action first unknown case measures again" {
+    assert jam_action(10) == ACT_MEASURE_AGAIN;
+}
+
+test "action unknown case 200 measures again" {
+    assert jam_action(200) == ACT_MEASURE_AGAIN;
+}
+
+// ---- jam_is_queens_turn ----
+
+test "queens turn for no reviewer" {
+    assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
+}
+
+test "queens turn for refused not fixed" {
+    assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
+}
+
+test "queens turn for refused then pushed" {
+    assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
+}
+
+test "queens turn for labeled not merged" {
+    assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
+}
+
+test "queens turn for conflicting" {
+    assert jam_is_queens_turn(JAM_CONFLICTING) == true;
+}
+
+test "queens turn for noise red" {
+    assert jam_is_queens_turn(JAM_NOISE_RED) == true;
+}
+
+test "queens turn for unmeasured" {
+    assert jam_is_queens_turn(JAM_UNMEASURED) == true;
+}
+
+test "not queens turn for none" {
+    assert jam_is_queens_turn(JAM_NONE) == false;
+}
+
+test "not queens turn for draft or fresh" {
+    assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
+}
+
+test "not queens turn for stacked on open" {
+    assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
+}
+
+// ---- the two scenarios of #9032, end to end ----
+
+test "scenario bee-reviewed but still open finishes the merge" {
+    assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE;
+}
+
+test "scenario failed read is never reported as free" {
+    assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-queue/card.png b/apps/website/public/ru/learn/the-queue/card.png new file mode 100644 index 0000000000..26dd50c8de Binary files /dev/null and b/apps/website/public/ru/learn/the-queue/card.png differ diff --git a/apps/website/public/ru/learn/the-queue/index.html b/apps/website/public/ru/learn/the-queue/index.html new file mode 100644 index 0000000000..4135661df5 --- /dev/null +++ b/apps/website/public/ru/learn/the-queue/index.html @@ -0,0 +1,238 @@ + + + + + + +Очередь: курс «Королева и улей», урок 5 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Очередь

+

Вы узнаете

Как потолок, старение и блокеры упорядочивают очередь открытых задач.

+

Метки раздуваются: если каждая задача критична, критичных нет. capped_level позволяет идти как critical не больше чем CRITICAL_CAP, 3, задачам списка, в порядке списка, а 4-я идёт как high. Задача с открытым блокером пропускается, её не ждут, и она не может задержать очередь. outranks сравнивает уровень с учётом старения, затем настоящий уровень, затем место в списке, так что постаревшая задача никогда не обгонит настоящую своего уровня. why называет причину каждого места.

+

Попробовать

Сделайте критичной пятую задачу и посмотрите, куда она встанет; затем поставьте задаче без метки метку low.

+

Открыть интерактивный урок →

+
The queue: which issue the Queen takes first
The queue: which issue the Queen takes first ↗
+

Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there.

+

specs/queen/priority.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366)
+; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no
+; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it.
+; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js`
+; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes.
+; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank,
+; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort):
+;   - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first);
+;   - an issue with an open blocker is skipped, not waited on (head-of-line blocking);
+;   - a cap on critical stops priority inflation; aging stops starvation of labelled low work;
+;   - an issue nobody labelled keeps today's place, so a repository with no labels sees no change.
+; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority
+; field (read it first once the org enables it; its four values map onto the same levels).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenPriority;
+
+/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages.
+pub const LEVEL_CRITICAL : u8 = 0;
+pub const LEVEL_HIGH : u8 = 1;
+pub const LEVEL_NORMAL : u8 = 2;
+pub const LEVEL_LOW : u8 = 3;
+
+/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing.
+/// When an issue carries labels of two levels, the more urgent one counts.
+pub const CRITICAL_LABELS : str = "P0,priority/critical,critical";
+pub const HIGH_LABELS : str = "P1,priority/high,high-priority";
+pub const NORMAL_LABELS : str = "P2,priority/medium";
+pub const LOW_LABELS : str = "P3,P4,priority/low";
+
+/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels,
+/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low"
+/// the fastest way to the front of a long queue.
+pub const AGING_DAYS : u32 = 14;
+pub const MAX_AGING_STEPS : u32 = 1;
+
+/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH.
+pub const CRITICAL_CAP : u32 = 3;
+
+/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs.
+pub const WHY_LISTING : u8 = 0;
+pub const WHY_LABEL : u8 = 1;
+pub const WHY_AGED : u8 = 2;
+pub const WHY_CAPPED : u8 = 3;
+pub const WHY_BLOCKED : u8 = 4;
+
+/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one.
+pub fn capped_level(base: u8, criticals_before: u32) -> u8 {
+    if (base == LEVEL_CRITICAL) {
+        if (criticals_before >= CRITICAL_CAP) {
+            return LEVEL_HIGH;
+        }
+    }
+    return base;
+}
+
+/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS)
+/// when it is labelled.
+pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 {
+    if (labelled == false) {
+        return level;
+    }
+    if (level <= LEVEL_HIGH) {
+        return level;
+    }
+    var steps : u32 = age_days / AGING_DAYS;
+    if (steps > MAX_AGING_STEPS) {
+        steps = MAX_AGING_STEPS;
+    }
+    var gap : u32 = (level as u32) - (LEVEL_HIGH as u32);
+    if (steps >= gap) {
+        return LEVEL_HIGH;
+    }
+    return ((level as u32) - steps) as u8;
+}
+
+/// An issue with an open blocker is not a candidate; the Queen moves past it.
+pub fn eligible(open_blockers: u32) -> bool {
+    return open_blockers == 0;
+}
+
+/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged
+/// issue never beats a real one of the same level), then earlier in GitHub's listing.
+pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool {
+    if (eff_a < eff_b) { return true; }
+    if (eff_a > eff_b) { return false; }
+    if (level_a < level_b) { return true; }
+    if (level_a > level_b) { return false; }
+    return index_a < index_b;
+}
+
+/// The reason printed for one issue.
+pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 {
+    if (open_blockers > 0) { return WHY_BLOCKED; }
+    if (level != base) { return WHY_CAPPED; }
+    if (eff != level) { return WHY_AGED; }
+    if (labelled) { return WHY_LABEL; }
+    return WHY_LISTING;
+}
+
+test no_label_keeps_the_listing_order {
+    assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1);
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false;
+    // #7077: a strict order, so a sort can use it: no issue outranks itself
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false;
+}
+
+test critical_runs_before_a_newer_unlabelled_issue {
+    assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0);
+}
+
+test unlabelled_never_beats_urgent {
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false;
+}
+
+test labelled_work_ages_one_level_and_never_to_critical {
+    assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW;
+    assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL;
+    assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH;
+    assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH;
+}
+
+test an_aged_issue_loses_the_tie_to_a_real_one {
+    var aged : u8 = effective_level(LEVEL_LOW, true, 28);
+    assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false;
+    assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0);
+    // #7077: of two LOW labels the aged one goes first, whatever the listing says
+    assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0);
+    assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false;
+}
+
+test the_fourth_critical_runs_as_high {
+    assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL;
+    assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH;
+    assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW;
+}
+
+test a_blocked_issue_is_skipped {
+    assert eligible(0);
+    assert eligible(1) == false;
+    assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED;
+}
+
+test every_reason_is_reachable {
+    assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING;
+    assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL;
+    assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED;
+    assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED;
+}
+
+invariant levels_are_ordered {
+    assert LEVEL_CRITICAL < LEVEL_HIGH;
+    assert LEVEL_HIGH < LEVEL_NORMAL;
+    assert LEVEL_NORMAL < LEVEL_LOW;
+    assert CRITICAL_CAP > 0;
+    assert AGING_DAYS > 0;
+    assert MAX_AGING_STEPS == 1;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-review-jam/card.png b/apps/website/public/ru/learn/the-review-jam/card.png new file mode 100644 index 0000000000..b67b84752b Binary files /dev/null and b/apps/website/public/ru/learn/the-review-jam/card.png differ diff --git a/apps/website/public/ru/learn/the-review-jam/index.html b/apps/website/public/ru/learn/the-review-jam/index.html new file mode 100644 index 0000000000..75ed987c76 --- /dev/null +++ b/apps/website/public/ru/learn/the-review-jam/index.html @@ -0,0 +1,349 @@ + + + + + + +Затор ревью: курс «Королева и улей», урок 13 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Затор ревью

+

Вы узнаете

В каком из десяти случаев застрявший пулл-реквест и почему побеждает правило, совпавшее раньше остальных.

+

Пулл-реквест может застрять сразу по нескольким причинам: отклонён, снова запушен и красный на шумовой проверке. review-jam.t27 даёт Королеве один ответ. jam_case проверяет 9 фактов в постоянном порядке, и побеждает правило, совпавшее раньше остальных, так что PR, который Королева не смогла измерить, никогда не считается свободным, а отклонённый и затем запушенный не принимается за шум. Случаев 10, от none до unmeasured.

+

Попробовать

Снимайте факты по одному со стартового состояния и называйте каждый случай; затем найдите PR, который ни в каком заторе.

+

Открыть интерактивный урок →

+
The review jam: which case a stuck pull request is in
The review jam: which case a stuck pull request is in ↗
+

Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule.

+

specs/automation/review-jam.t27

// Review jam: which case a stuck pull request is in, and the Queen's action.
+// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700).
+// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead
+// of hand-copying them.
+
+module review_jam;
+
+// Jam case constants (u8, value = position, starting at 0)
+pub const JAM_NONE: u8 = 0;
+pub const JAM_NO_REVIEWER: u8 = 1;
+pub const JAM_STACKED_ON_OPEN: u8 = 2;
+pub const JAM_REFUSED_NOT_FIXED: u8 = 3;
+pub const JAM_REFUSED_THEN_PUSHED: u8 = 4;
+pub const JAM_LABELED_NOT_MERGED: u8 = 5;
+pub const JAM_CONFLICTING: u8 = 6;
+pub const JAM_NOISE_RED: u8 = 7;
+pub const JAM_DRAFT_OR_FRESH: u8 = 8;
+pub const JAM_UNMEASURED: u8 = 9;
+
+// Queen action constants (u8, value = position, starting at 0)
+pub const ACT_NOTHING: u8 = 0;
+pub const ACT_DISPATCH_REVIEWER: u8 = 1;
+pub const ACT_WAIT_FOR_BASE: u8 = 2;
+pub const ACT_DISPATCH_CODER: u8 = 3;
+pub const ACT_DISPATCH_RE_REVIEW: u8 = 4;
+pub const ACT_FINISH_MERGE: u8 = 5;
+pub const ACT_REBASE_BY_CODER: u8 = 6;
+pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7;
+pub const ACT_WAIT: u8 = 8;
+pub const ACT_MEASURE_AGAIN: u8 = 9;
+
+// The jam case of a pull request. The FIRST matching rule wins:
+//  1. not measured                      -> UNMEASURED
+//  2. draft or fresh                    -> DRAFT_OR_FRESH
+//  3. stacked on an open PR             -> STACKED_ON_OPEN
+//  4. conflicting                       -> CONFLICTING
+//  5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
+//  6. refused and pushed after          -> REFUSED_THEN_PUSHED
+//  7. refused                           -> REFUSED_NOT_FIXED
+//  8. only noise checks red             -> NOISE_RED
+//  9. no reviewer                       -> NO_REVIEWER
+// 10. otherwise                         -> NONE
+pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 {
+    if !measured {
+        return JAM_UNMEASURED;
+    }
+    if draft_or_fresh {
+        return JAM_DRAFT_OR_FRESH;
+    }
+    if stacked_on_open {
+        return JAM_STACKED_ON_OPEN;
+    }
+    if conflicting {
+        return JAM_CONFLICTING;
+    }
+    if labeled_reviewed {
+        return JAM_LABELED_NOT_MERGED;
+    }
+    if refused && pushed_after_refusal {
+        return JAM_REFUSED_THEN_PUSHED;
+    }
+    if refused {
+        return JAM_REFUSED_NOT_FIXED;
+    }
+    if only_noise_red {
+        return JAM_NOISE_RED;
+    }
+    if !has_reviewer {
+        return JAM_NO_REVIEWER;
+    }
+    JAM_NONE
+}
+
+// The Queen's action for a case: the action with the SAME position.
+// Any value above 9 is an unknown case, and an unknown case is never
+// "nothing to do": it maps to ACT_MEASURE_AGAIN.
+pub fn jam_action(case: u8) -> u8 {
+    if case > 9 {
+        return ACT_MEASURE_AGAIN;
+    }
+    case
+}
+
+// Whether the case is the Queen's turn to act: true for every case except
+// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN.
+pub fn jam_is_queens_turn(case: u8) -> bool {
+    case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN
+}
+
+// ---- jam_case: one test per case ----
+
+test "unmeasured beats every other flag" {
+    assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
+}
+
+test "unmeasured with every other flag clear" {
+    assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED;
+}
+
+test "draft or fresh" {
+    assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked on open PR" {
+    assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting" {
+    assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled not merged" {
+    assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed" {
+    assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed" {
+    assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red" {
+    assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED;
+}
+
+test "no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER;
+}
+
+test "none" {
+    assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE;
+}
+
+// ---- jam_case: first-match order, each rule against the next one ----
+
+test "draft beats stacked" {
+    assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH;
+}
+
+test "stacked and unreviewed is stacked, not no reviewer" {
+    assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "stacked beats conflicting" {
+    assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
+}
+
+test "conflicting beats labeled" {
+    assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING;
+}
+
+test "labeled beats refused then pushed" {
+    assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED;
+}
+
+test "refused then pushed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED;
+}
+
+test "refused not fixed beats noise red" {
+    assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED;
+}
+
+test "noise red beats no reviewer" {
+    assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
+}
+
+test "pushed without a refusal is not refused then pushed" {
+    assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE;
+}
+
+// ---- jam_action: same position, unknown -> measure again ----
+
+test "action none to nothing" {
+    assert jam_action(JAM_NONE) == ACT_NOTHING;
+}
+
+test "action no reviewer to dispatch reviewer" {
+    assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
+}
+
+test "action stacked to wait for base" {
+    assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE;
+}
+
+test "action refused not fixed to dispatch coder" {
+    assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER;
+}
+
+test "action refused then pushed to dispatch re-review" {
+    assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW;
+}
+
+test "action labeled not merged to finish merge" {
+    assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE;
+}
+
+test "action conflicting to rebase by coder" {
+    assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER;
+}
+
+test "action noise red to name noise and review" {
+    assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW;
+}
+
+test "action draft or fresh to wait" {
+    assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT;
+}
+
+test "action unmeasured to measure again" {
+    assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN;
+}
+
+test "action first unknown case measures again" {
+    assert jam_action(10) == ACT_MEASURE_AGAIN;
+}
+
+test "action unknown case 200 measures again" {
+    assert jam_action(200) == ACT_MEASURE_AGAIN;
+}
+
+// ---- jam_is_queens_turn ----
+
+test "queens turn for no reviewer" {
+    assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
+}
+
+test "queens turn for refused not fixed" {
+    assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
+}
+
+test "queens turn for refused then pushed" {
+    assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
+}
+
+test "queens turn for labeled not merged" {
+    assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
+}
+
+test "queens turn for conflicting" {
+    assert jam_is_queens_turn(JAM_CONFLICTING) == true;
+}
+
+test "queens turn for noise red" {
+    assert jam_is_queens_turn(JAM_NOISE_RED) == true;
+}
+
+test "queens turn for unmeasured" {
+    assert jam_is_queens_turn(JAM_UNMEASURED) == true;
+}
+
+test "not queens turn for none" {
+    assert jam_is_queens_turn(JAM_NONE) == false;
+}
+
+test "not queens turn for draft or fresh" {
+    assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
+}
+
+test "not queens turn for stacked on open" {
+    assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
+}
+
+// ---- the two scenarios of #9032, end to end ----
+
+test "scenario bee-reviewed but still open finishes the merge" {
+    assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE;
+}
+
+test "scenario failed read is never reported as free" {
+    assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/the-ship-flies-home/card.png b/apps/website/public/ru/learn/the-ship-flies-home/card.png new file mode 100644 index 0000000000..02b82b1aa6 Binary files /dev/null and b/apps/website/public/ru/learn/the-ship-flies-home/card.png differ diff --git a/apps/website/public/ru/learn/the-ship-flies-home/index.html b/apps/website/public/ru/learn/the-ship-flies-home/index.html new file mode 100644 index 0000000000..cc84555460 --- /dev/null +++ b/apps/website/public/ru/learn/the-ship-flies-home/index.html @@ -0,0 +1,259 @@ + + + + + + +Корабль летит домой: курс «Королева и улей», урок 8 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Корабль летит домой

+

Вы узнаете

Где игра рисует пчелу в каждом её занятии и как завершённая задача заканчивается на экране.

+

В игре на app.t27.ai/game каждая пчела — корабль над задачей, над которой она работает. ship_mode в hive.t27 выбирает место: стоянка у входа, пока раннер её не взял, облёт своей ячейки, пока от неё приходят вести, зависание, когда она замолчала, и ожидание у портала своего репозитория, пока ячейку не поставили. Когда пчела уходит из ленты, она летит домой, и ship_alpha гасит её за DEPART_MS, 2400 мс: через 1200 мс у неё 500 из 1000.

+

Попробовать

Отметьте, что от пчелы давно нет вестей, и посмотрите, куда она сдвинется; затем протяните время дальше 2400 мс.

+

Открыть интерактивный урок →

+
The game: where a bee flies and how it leaves
The game: where a bee flies and how it leaves ↗
+

Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out.

+

specs/queen/hive.t27

// SPDX-License-Identifier: Apache-2.0
+; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to
+; a cell, how a ship leaves, and how often the map may grow
+; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game
+; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4.
+; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day):
+; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen.
+; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far
+;   from the repository it works for.
+; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole
+;   scene: one rebuild for most bees that started.
+; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same
+;   as no news.
+; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two
+; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm.
+; ASCII only (L3).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module QueenHive;
+
+pub const HIVE_KINDS : u8 = 10;
+
+; --- 1. Where a ship flies ---------------------------------------------------------------------------
+
+/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this
+/// map can show (a repository with no region, or a task that is not an issue).
+pub const CELL_PLACED : u8 = 0;
+pub const CELL_PENDING : u8 = 1;
+pub const CELL_NONE : u8 = 2;
+
+/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim,
+/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is
+/// placed. HOME: flying back to the entrance, its task over.
+pub const SM_PARK : u8 = 0;
+pub const SM_ORBIT : u8 = 1;
+pub const SM_HOVER : u8 = 2;
+pub const SM_PORTAL : u8 = 3;
+pub const SM_HOME : u8 = 4;
+
+/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no
+/// longer in the feed.
+pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 {
+    if (departing) { return SM_HOME; }
+    if (queued) { return SM_PARK; }
+    if (cell == CELL_PENDING) { return SM_PORTAL; }
+    if (cell != CELL_PLACED) { return SM_PARK; }
+    if (working) { return SM_ORBIT; }
+    return SM_HOVER;
+}
+
+; --- 2. How a ship leaves ----------------------------------------------------------------------------
+
+/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing.
+pub const DEPART_MS : u32 = 2400;
+
+pub fn ship_kept(ms_since_left: u32) -> bool {
+    return ms_since_left < DEPART_MS;
+}
+
+/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing.
+pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 {
+    if (departing == false) { return 1000; }
+    if (ms_since_left >= DEPART_MS) { return 0; }
+    return 1000 - (ms_since_left * 1000) / DEPART_MS;
+}
+
+; --- 3. What an event does to a cell -----------------------------------------------------------------
+
+/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the
+/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is
+/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it.
+pub const FX_NONE : u8 = 0;
+pub const FX_START : u8 = 1;
+pub const FX_ACCEPT : u8 = 2;
+pub const FX_SEND_BACK : u8 = 3;
+pub const FX_LOST : u8 = 4;
+pub const FX_CANCEL : u8 = 5;
+
+/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by
+/// its verdict, cancel CANCEL; everything else nothing.
+pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0];
+
+/// `accepted`: a review's verdict was an acceptance. Only a review reads it.
+pub fn cell_effect(kind: u8, accepted: bool) -> u8 {
+    if (kind >= HIVE_KINDS) { return FX_NONE; }
+    let fx : u8 = FX_OF_KIND[kind];
+    if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; }
+    return fx;
+}
+
+/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn.
+pub const FLASH_MS : u32 = 1800;
+
+pub fn flash_alpha(ms_since: u32) -> u32 {
+    if (ms_since >= FLASH_MS) { return 0; }
+    return 1000 - (ms_since * 1000) / FLASH_MS;
+}
+
+; --- 4. How often the map may grow -------------------------------------------------------------------
+
+/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding
+/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS --
+/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of
+/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's
+/// portal (SM_PORTAL), so where it is going is already true.
+pub const CELL_BATCH_SECONDS : u32 = 300;
+
+pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool {
+    if (pending == 0) { return false; }
+    if (placed_before == false) { return true; }
+    return seconds_since_batch >= CELL_BATCH_SECONDS;
+}
+
+test a_ship_flies_where_its_bee_is {
+    // queued: at the entrance, wherever its cell stands
+    assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK;
+    assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK;
+    // claimed, its cell placed: orbit while heard, hover when quiet
+    assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT;
+    assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER;
+    // its cell not placed yet: at its repository's portal, working or not
+    assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL;
+    assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL;
+    // no cell this map can show: back at the entrance
+    assert ship_mode(false, true, CELL_NONE, false) == SM_PARK;
+    // gone from the feed: home, whatever it was
+    assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME;
+    assert ship_mode(true, false, CELL_NONE, true) == SM_HOME;
+}
+
+test a_ship_leaves_by_flying_home {
+    assert ship_kept(0) == true;
+    assert ship_kept(DEPART_MS - 1) == true;
+    assert ship_kept(DEPART_MS) == false;
+    assert ship_alpha(false, 0) == 1000;
+    assert ship_alpha(false, 99999) == 1000;
+    assert ship_alpha(true, 0) == 1000;
+    assert ship_alpha(true, 1200) == 500;
+    assert ship_alpha(true, DEPART_MS - 1) == 1;
+    assert ship_alpha(true, DEPART_MS) == 0;
+    assert ship_alpha(true, 4000000000) == 0;
+}
+
+test an_event_lights_its_cell {
+    assert cell_effect(0, false) == FX_START;
+    assert cell_effect(1, true) == FX_NONE;
+    assert cell_effect(2, false) == FX_NONE;
+    assert cell_effect(3, false) == FX_LOST;
+    assert cell_effect(4, true) == FX_NONE;
+    assert cell_effect(5, true) == FX_ACCEPT;
+    assert cell_effect(5, false) == FX_SEND_BACK;
+    assert cell_effect(6, true) == FX_NONE;
+    assert cell_effect(7, false) == FX_CANCEL;
+    assert cell_effect(8, false) == FX_NONE;
+    assert cell_effect(9, true) == FX_NONE;
+    // only a review reads the verdict
+    assert cell_effect(3, true) == FX_LOST;
+    assert cell_effect(HIVE_KINDS, true) == FX_NONE;
+}
+
+test a_flash_fades {
+    assert flash_alpha(0) == 1000;
+    assert flash_alpha(900) == 500;
+    assert flash_alpha(FLASH_MS - 1) == 1;
+    assert flash_alpha(FLASH_MS) == 0;
+    assert flash_alpha(4000000000) == 0;
+}
+
+test the_map_grows_in_batches {
+    assert cells_due(0, false, 0) == false;
+    assert cells_due(0, true, 99999) == false;
+    // the first answer places every cell at once
+    assert cells_due(3, false, 0) == true;
+    // after that, one batch a period
+    assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false;
+    assert cells_due(1, true, CELL_BATCH_SECONDS) == true;
+}
+
+invariant one_row_per_kind {
+    assert HIVE_KINDS == 10;
+    assert FX_OF_KIND[5] == FX_ACCEPT;
+    assert FLASH_MS < DEPART_MS * 2;
+    assert CELL_BATCH_SECONDS == 300;
+}
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/three-bots-deep/card.png b/apps/website/public/ru/learn/three-bots-deep/card.png new file mode 100644 index 0000000000..7c139ef64a Binary files /dev/null and b/apps/website/public/ru/learn/three-bots-deep/card.png differ diff --git a/apps/website/public/ru/learn/three-bots-deep/index.html b/apps/website/public/ru/learn/three-bots-deep/index.html new file mode 100644 index 0000000000..ecac973c0f --- /dev/null +++ b/apps/website/public/ru/learn/three-bots-deep/index.html @@ -0,0 +1,436 @@ + + + + + + +Три бота в глубину: курс «Королева и улей», урок 23 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Три бота в глубину

+

Вы узнаете

Как улей не даёт агентам отвечать друг другу бесконечно.

+

Telegram не защищает ботов от зацикливания, так что два агента могли бы отвечать друг другу вечно. Это останавливают два числа. MAX_BOT_DEPTH равно 3: человек, затем A, B и C, затем тишина. BOT_REPLY_COOLDOWN_MS равно 5000: бот отвечает другому боту не чаще раза в 5 с. may_answer всегда пропускает человека, а бота — только внутри цепочки и после паузы.

+

Попробовать

Найдите промежуток, при котором агент B замолкает; затем объясните, почему молчит и C.

+

Открыть интерактивный урок →

+
Chain depth: human, A, B, C, then silence
Chain depth: human, A, B, C, then silence ↗
+

One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/tokens-that-burn/card.png b/apps/website/public/ru/learn/tokens-that-burn/card.png new file mode 100644 index 0000000000..ceecb16b5b Binary files /dev/null and b/apps/website/public/ru/learn/tokens-that-burn/card.png differ diff --git a/apps/website/public/ru/learn/tokens-that-burn/index.html b/apps/website/public/ru/learn/tokens-that-burn/index.html new file mode 100644 index 0000000000..c047af71ff --- /dev/null +++ b/apps/website/public/ru/learn/tokens-that-burn/index.html @@ -0,0 +1,214 @@ + + + + + + +Сгорающие токены: курс «Королева и улей», урок 21 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Сгорающие токены

+

Вы узнаете

Когда сгорает партия токенов и какая партия тратится раньше.

+

Правило владельца от 2026-10-01: все деньги уходят в токены, а токены, не потраченные за месяц, сгорают, так что заплативший делает контент, а не копит токены. Каждая партия живёт MONTH_DAYS, 30 дней, и days_left отсчитывает её от дня зачисления. На 25-й день у пакета месяца с дня 0 осталось 5 дней, у стартового подарка с дня 3 — 8, у пополнения с дня 12 — 17. Тратится раньше та, что сгорит раньше.

+

Попробовать

Найдите день, когда сгорает пакет месяца; затем найдите последний день, когда остаётся хоть одна партия.

+

Открыть интерактивный урок →

+
Tokens that burn: a month to spend them
Tokens that burn: a month to spend them ↗
+

Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first.

+

specs/automation/token-wallet.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a
+// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and
+// tokens that are not spent within a month burn.
+// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts
+//       (the month a package buys, what it is worth in reels and portraits, when it burns),
+//       read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx,
+//       player/src/components/Agents/PackageCards.tsx) and by render's billing
+//       (render/src/agent/billing-shared.ts: the rate and the markup every price is made with).
+// Generated: `t27c gen-ts` of this file is vendored into the host as
+//       apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports
+//       the numbers from there and never restates them; a host test runs this file's vectors
+//       against the host.
+// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent
+// within the month or they burn -- so a person who paid makes content instead of saving
+// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the
+// credits that burn soonest first, and so does this.
+// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts
+// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN).
+// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call.
+// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host
+// already charges. The month, the burn and the spend order are DECIDED but not yet in the
+// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an
+// expiry are the next change, and the burn must not be sold before it lands.
+// Owner decision still open: whether tokens bought on top of a package burn in 30 days like
+// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's
+// bought credits do (TOPUP_DAYS is the one line to change).
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::token_wallet {
+
+    pub const KIND : str = "automation";
+    pub const ID : str = "token-wallet";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 1;
+
+    // ---- One rate, one markup -----------------------------------------------------------
+    pub const TOKEN_MICRO_USD : u32 = 5000;  // one token is half a cent, everywhere
+    pub const MARKUP_PCT : u32 = 200;  // an operation is sold at twice what it costs us
+    pub const MICRO_PER_USD : u32 = 1000000;
+
+    // ---- What a month buys ----------------------------------------------------------------
+    // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is
+    // kept back, the margin is the markup on what is spent and whatever burns.
+    pub const SETUP_INCLUDES_MONTH : bool = true;  // the launch price carries the first month
+
+    // ---- The burn -----------------------------------------------------------------------
+    pub const MONTH_DAYS : u16 = 30;  // a package month burns 30 days after it lands
+    pub const TOPUP_DAYS : u16 = 30;  // owner's rule: bought tokens burn the same way
+    pub const START_DAYS : u16 = 30;  // the start gift burns too
+    pub const SPEND_ORDER : str = "soonest-burn-first";
+    pub const BURNS_BACKDATED : bool = false;  // tokens held before v1 never burn
+    pub const WARN_DAYS_FIRST : u16 = 7;
+    pub const WARN_DAYS_SECOND : u16 = 3;
+    pub const WARN_DAYS_LAST : u16 = 1;
+
+    // ---- The start gift -------------------------------------------------------------------
+    pub const START_TOKENS : u32 = 20;  // granted once, on the first paid call
+
+    // ---- What tokens are worth, in things a person can picture ----------------------------
+    // The reference reel is the story reel's own estimate (crm-story-reel.t27
+    // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the
+    // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds
+    // both to those specs, so the welcome screen cannot promise a reel at an old price.
+    pub const REEL_TOKENS : u32 = 270;
+    pub const PORTRAIT_TOKENS : u32 = 12;
+
+    // Dollars a month to tokens a month.
+    fn month_tokens(usd: u32) -> u32 {
+        return usd * MICRO_PER_USD / TOKEN_MICRO_USD;
+    }
+
+    // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so
+    // nothing is sold below twice its cost.
+    fn price_tokens(cost_micro_usd: u32) -> u32 {
+        return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD;
+    }
+
+    // Whole reels a number of tokens buys.
+    fn reels_in(tokens: u32) -> u32 {
+        return tokens / REEL_TOKENS;
+    }
+
+    // Whole portraits a number of tokens buys.
+    fn portraits_in(tokens: u32) -> u32 {
+        return tokens / PORTRAIT_TOKENS;
+    }
+
+    // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone).
+    fn days_left(granted_day: u16, today: u16, life: u16) -> u16 {
+        if (today >= granted_day + life) {
+            return 0;
+        }
+        return granted_day + life - today;
+    }
+
+    test "every dollar of a package month becomes tokens at the one rate" {
+        assert(month_tokens(15) == 3000);
+        assert(month_tokens(35) == 7000);
+        assert(month_tokens(50) == 10000);
+        assert(SETUP_INCLUDES_MONTH);
+    }
+
+    test "an operation is sold at twice its cost, rounded up" {
+        assert(MARKUP_PCT == 200);
+        assert(price_tokens(30000) == 12);
+        assert(price_tokens(20000) == 8);
+        assert(price_tokens(1) == 1);
+    }
+
+    test "a month burns on day thirty and nothing held before it burns" {
+        assert(days_left(0, 0, MONTH_DAYS) == 30);
+        assert(days_left(0, 23, MONTH_DAYS) == 7);
+        assert(days_left(0, 30, MONTH_DAYS) == 0);
+        assert(TOPUP_DAYS == MONTH_DAYS);
+        assert(SPEND_ORDER == "soonest-burn-first");
+        assert(!BURNS_BACKDATED);
+    }
+
+    test "the warnings come at seven, three and one day left" {
+        assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND);
+        assert(WARN_DAYS_SECOND > WARN_DAYS_LAST);
+        assert(WARN_DAYS_FIRST < MONTH_DAYS);
+    }
+
+    test "the start gift buys one portrait and no reel" {
+        assert(portraits_in(START_TOKENS) == 1);
+        assert(reels_in(START_TOKENS) == 0);
+    }
+
+    test "a Blogger month is twenty-five reels" {
+        assert(reels_in(month_tokens(35)) == 25);
+        assert(reels_in(month_tokens(15)) == 11);
+        assert(reels_in(month_tokens(50)) == 37);
+        assert(portraits_in(month_tokens(35)) == 583);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/twice-the-cost/card.png b/apps/website/public/ru/learn/twice-the-cost/card.png new file mode 100644 index 0000000000..b4c65e5256 Binary files /dev/null and b/apps/website/public/ru/learn/twice-the-cost/card.png differ diff --git a/apps/website/public/ru/learn/twice-the-cost/index.html b/apps/website/public/ru/learn/twice-the-cost/index.html new file mode 100644 index 0000000000..2a88db9a1a --- /dev/null +++ b/apps/website/public/ru/learn/twice-the-cost/index.html @@ -0,0 +1,214 @@ + + + + + + +Вдвое дороже цены: курс «Королева и улей», урок 20 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Вдвое дороже цены

+

Вы узнаете

Как проект назначает цену операции и что покупает месяц токенов.

+

Каждый доллар становится токенами по одному курсу, TOKEN_MICRO_USD 5000: токен — полцента. price_tokens продаёт операцию вдвое дороже её стоимости, MARKUP_PCT 200, и округляет вверх, чтобы ничего не продавалось дешевле: стоимость 12300 микродолларов — это 5 токенов. month_tokens превращает $20 в 4000 токенов, то есть 14 рилсов по 270 токенов или 333 портрета по 12.

+

Попробовать

Найдите стоимости, которые дают ровно 1 токен; затем найдите месячную цену, на которую выходит 36 рилсов.

+

Открыть интерактивный урок →

+
The price of an operation: twice its cost, rounded up
The price of an operation: twice its cost, rounded up ↗
+

Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens.

+

specs/automation/token-wallet.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a
+// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and
+// tokens that are not spent within a month burn.
+// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts
+//       (the month a package buys, what it is worth in reels and portraits, when it burns),
+//       read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx,
+//       player/src/components/Agents/PackageCards.tsx) and by render's billing
+//       (render/src/agent/billing-shared.ts: the rate and the markup every price is made with).
+// Generated: `t27c gen-ts` of this file is vendored into the host as
+//       apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports
+//       the numbers from there and never restates them; a host test runs this file's vectors
+//       against the host.
+// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent
+// within the month or they burn -- so a person who paid makes content instead of saving
+// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the
+// credits that burn soonest first, and so does this.
+// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts
+// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN).
+// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call.
+// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host
+// already charges. The month, the burn and the spend order are DECIDED but not yet in the
+// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an
+// expiry are the next change, and the burn must not be sold before it lands.
+// Owner decision still open: whether tokens bought on top of a package burn in 30 days like
+// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's
+// bought credits do (TOPUP_DAYS is the one line to change).
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::token_wallet {
+
+    pub const KIND : str = "automation";
+    pub const ID : str = "token-wallet";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 1;
+
+    // ---- One rate, one markup -----------------------------------------------------------
+    pub const TOKEN_MICRO_USD : u32 = 5000;  // one token is half a cent, everywhere
+    pub const MARKUP_PCT : u32 = 200;  // an operation is sold at twice what it costs us
+    pub const MICRO_PER_USD : u32 = 1000000;
+
+    // ---- What a month buys ----------------------------------------------------------------
+    // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is
+    // kept back, the margin is the markup on what is spent and whatever burns.
+    pub const SETUP_INCLUDES_MONTH : bool = true;  // the launch price carries the first month
+
+    // ---- The burn -----------------------------------------------------------------------
+    pub const MONTH_DAYS : u16 = 30;  // a package month burns 30 days after it lands
+    pub const TOPUP_DAYS : u16 = 30;  // owner's rule: bought tokens burn the same way
+    pub const START_DAYS : u16 = 30;  // the start gift burns too
+    pub const SPEND_ORDER : str = "soonest-burn-first";
+    pub const BURNS_BACKDATED : bool = false;  // tokens held before v1 never burn
+    pub const WARN_DAYS_FIRST : u16 = 7;
+    pub const WARN_DAYS_SECOND : u16 = 3;
+    pub const WARN_DAYS_LAST : u16 = 1;
+
+    // ---- The start gift -------------------------------------------------------------------
+    pub const START_TOKENS : u32 = 20;  // granted once, on the first paid call
+
+    // ---- What tokens are worth, in things a person can picture ----------------------------
+    // The reference reel is the story reel's own estimate (crm-story-reel.t27
+    // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the
+    // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds
+    // both to those specs, so the welcome screen cannot promise a reel at an old price.
+    pub const REEL_TOKENS : u32 = 270;
+    pub const PORTRAIT_TOKENS : u32 = 12;
+
+    // Dollars a month to tokens a month.
+    fn month_tokens(usd: u32) -> u32 {
+        return usd * MICRO_PER_USD / TOKEN_MICRO_USD;
+    }
+
+    // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so
+    // nothing is sold below twice its cost.
+    fn price_tokens(cost_micro_usd: u32) -> u32 {
+        return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD;
+    }
+
+    // Whole reels a number of tokens buys.
+    fn reels_in(tokens: u32) -> u32 {
+        return tokens / REEL_TOKENS;
+    }
+
+    // Whole portraits a number of tokens buys.
+    fn portraits_in(tokens: u32) -> u32 {
+        return tokens / PORTRAIT_TOKENS;
+    }
+
+    // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone).
+    fn days_left(granted_day: u16, today: u16, life: u16) -> u16 {
+        if (today >= granted_day + life) {
+            return 0;
+        }
+        return granted_day + life - today;
+    }
+
+    test "every dollar of a package month becomes tokens at the one rate" {
+        assert(month_tokens(15) == 3000);
+        assert(month_tokens(35) == 7000);
+        assert(month_tokens(50) == 10000);
+        assert(SETUP_INCLUDES_MONTH);
+    }
+
+    test "an operation is sold at twice its cost, rounded up" {
+        assert(MARKUP_PCT == 200);
+        assert(price_tokens(30000) == 12);
+        assert(price_tokens(20000) == 8);
+        assert(price_tokens(1) == 1);
+    }
+
+    test "a month burns on day thirty and nothing held before it burns" {
+        assert(days_left(0, 0, MONTH_DAYS) == 30);
+        assert(days_left(0, 23, MONTH_DAYS) == 7);
+        assert(days_left(0, 30, MONTH_DAYS) == 0);
+        assert(TOPUP_DAYS == MONTH_DAYS);
+        assert(SPEND_ORDER == "soonest-burn-first");
+        assert(!BURNS_BACKDATED);
+    }
+
+    test "the warnings come at seven, three and one day left" {
+        assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND);
+        assert(WARN_DAYS_SECOND > WARN_DAYS_LAST);
+        assert(WARN_DAYS_FIRST < MONTH_DAYS);
+    }
+
+    test "the start gift buys one portrait and no reel" {
+        assert(portraits_in(START_TOKENS) == 1);
+        assert(reels_in(START_TOKENS) == 0);
+    }
+
+    test "a Blogger month is twenty-five reels" {
+        assert(reels_in(month_tokens(35)) == 25);
+        assert(reels_in(month_tokens(15)) == 11);
+        assert(reels_in(month_tokens(50)) == 37);
+        assert(portraits_in(month_tokens(35)) == 583);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/ru/learn/who-is-called/card.png b/apps/website/public/ru/learn/who-is-called/card.png new file mode 100644 index 0000000000..bfee80430b Binary files /dev/null and b/apps/website/public/ru/learn/who-is-called/card.png differ diff --git a/apps/website/public/ru/learn/who-is-called/index.html b/apps/website/public/ru/learn/who-is-called/index.html new file mode 100644 index 0000000000..056dfe47a0 --- /dev/null +++ b/apps/website/public/ru/learn/who-is-called/index.html @@ -0,0 +1,436 @@ + + + + + + +Кого зовут: курс «Королева и улей», урок 22 из 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
t27.aiEnglish
+
+ +

Кого зовут

+

Вы узнаете

Какие сообщения в группе улья будят агента — от человека и от бота другого агента.

+

У каждого агента свой бот и своя тема в одной форумной группе. Человек зовёт агента его тегом, через @all, ответом на сообщение или просто сообщением в его теме. Бот другого агента зовёт его только тегом или ответом: бот никогда не поднимает весь улей и не будит хозяина темы, просто написав в ней. addressed в agent-hive-group.t27 — это правило длиной в одну строку.

+

Попробовать

Найдите два способа, которыми агента может позвать только человек; затем включите все четыре и сравните отправителей.

+

Открыть интерактивный урок →

+
Who is called: an agent bot in a group of agents
Who is called: an agent bot in a group of agents ↗
+

Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot.

+

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
+// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
+// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
+// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
+// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
+// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
+// switched on in @BotFather for every bot, which no API call can do.
+// Telegram gives no loop protection between bots, so the depth and the cooldown
+// below are the only thing that stops two agents from answering each other forever.
+// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
+// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
+// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
+// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
+// a topic a person deleted, and /summary.
+// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
+// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
+// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
+// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
+// and only while its ask is fresh.
+// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
+// phi^2 + 1/phi^2 = 3 | TRINITY
+
+module automation::agent_hive_group {
+    pub const KIND : str = "automation";
+    pub const ID : str = "agent-hive-group";
+    pub const REPO : str = "999-multibots-telegraf";
+    pub const VERSION : u8 = 4;
+
+    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
+    pub const MAX_BOT_DEPTH : u32 = 3;
+    // One agent answers another agent at most this often.
+    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
+    // How long "create the bot for agent X" waits for Telegram's creation message.
+    pub const CLAIM_TTL_MS : u32 = 900000;
+    // The tag that calls every agent of the hive at once.
+    pub const ALL_TAG : str = "@all";
+    // Suggested username of an agent's bot: t27_<agent id>_bot.
+    pub const BOT_USERNAME_PREFIX : str = "t27_";
+    pub const BOT_USERNAME_SUFFIX : str = "_bot";
+
+    // A person may call an agent by its tag, by the all tag, by replying to it,
+    // or by writing in the agent's own topic. Another bot only by tag or reply:
+    // a bot never triggers the whole hive, and never wakes a topic's owner by
+    // merely writing there.
+    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
+        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
+    }
+
+    // depth = how many bot messages already stand between this one and the person.
+    fn chain_open(depth: u32) -> bool {
+        return depth < MAX_BOT_DEPTH;
+    }
+
+    fn cooled(since_last_ms: u32) -> bool {
+        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
+    }
+
+    // A person is always answered; a bot only inside the chain and after the cooldown.
+    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
+        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
+    }
+
+    fn claim_fresh(age_ms: u32) -> bool {
+        return age_ms < CLAIM_TTL_MS;
+    }
+
+    // ---- v2 ----
+
+    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
+    // The action is sent again this long after the last one, until the answer goes out.
+    pub const TYPING_REFRESH_MS : u32 = 4000;
+    // On the all tag only this many agents answer at once, best fit first; the rest
+    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
+    pub const ALL_FIRST_SPEAKERS : u32 = 1;
+    // A person's next message in the same topic goes to the agent that answered
+    // last, without a new tag, for this long.
+    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
+    // /stop: no bot answers a bot in that chat for this long. People are still answered.
+    pub const STOP_WINDOW_MS : u32 = 600000;
+    // /mute: the agent answers in that topic only when tagged by name, for this long.
+    pub const MUTE_MS : u32 = 3600000;
+
+    fn typing_due(since_last_ms: u32) -> bool {
+        return since_last_ms >= TYPING_REFRESH_MS;
+    }
+
+    // rank = the agent's place in the best-fit order for this message, from 0.
+    fn speaks_on_all(rank: u32) -> bool {
+        return rank < ALL_FIRST_SPEAKERS;
+    }
+
+    // A bot never follows up: it must tag or reply, as v1 says.
+    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
+        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
+    }
+
+    fn stopped(since_stop_ms: u32) -> bool {
+        return since_stop_ms < STOP_WINDOW_MS;
+    }
+
+    // A stop cuts bot-to-bot turns only; a person is always heard.
+    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
+        return !from_bot || !stopped(since_stop_ms);
+    }
+
+    fn muted(since_mute_ms: u32) -> bool {
+        return since_mute_ms < MUTE_MS;
+    }
+
+    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
+    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
+        return tagged || !muted(since_mute_ms);
+    }
+
+    // ---- v3 ----
+
+    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
+    // whether or not the reaction went through. The "seen" mark stays only when the
+    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
+    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
+        return answer_sent;
+    }
+
+    fn keeps_seen_mark(answer_sent: bool) -> bool {
+        return answer_sent;
+    }
+
+    // /brief: standing instructions for a group or one topic, set by an admin only.
+    pub const MAX_BRIEF_CHARS : u32 = 1000;
+    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
+    // The narrower one wins; the two are never glued together.
+    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
+        if has_topic_brief { return 2; }
+        if has_group_brief { return 1; }
+        return 0;
+    }
+
+    // An empty /brief is accepted: it clears the brief, it is not refused.
+    fn brief_fits(chars: u32) -> bool {
+        return chars <= MAX_BRIEF_CHARS;
+    }
+
+    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
+        return is_admin && !from_bot;
+    }
+
+    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
+    // Without rights the recreate fails every time, so it is tried at most this often.
+    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
+    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
+        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
+    }
+
+    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
+    pub const SUMMARY_LOG_SIZE : u32 = 200;
+    pub const SUMMARY_DEFAULT : u32 = 50;
+    fn summary_count(asked: u32) -> u32 {
+        if asked == 0 { return SUMMARY_DEFAULT; }
+        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
+        return asked;
+    }
+
+    // ---- v4: hand-off in private, bot to bot ----
+
+    // Every private hand-off between two agents starts with this tag, then its kind and the
+    // person it is for. Only a hive agent of the same owner can send it, so the person named in
+    // it is trusted: the turn runs in that person's name, never the owner's.
+    pub const DM_TAG : str = "#a2a";
+    pub const DM_ASK : u8 = 1;
+    pub const DM_ANSWER : u8 = 2;
+    // An answer to a person hands off to at most this many colleagues.
+    pub const MAX_DM_HANDOFFS : u32 = 2;
+
+    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
+    // the same owner) and its message carries the hand-off tag with a known kind.
+    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
+        return !from_bot || (colleague && tag_ok);
+    }
+
+    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
+    // chain is two messages long and cannot loop, whatever the agents write.
+    fn dm_runs_turn(kind: u8) -> bool {
+        return kind == DM_ASK;
+    }
+    fn dm_carries(kind: u8) -> bool {
+        return kind == DM_ANSWER;
+    }
+
+    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
+    // writes to a person nobody asked it about. Half an hour covers the longest turn.
+    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
+    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
+        if kind == DM_ASK { return true; }
+        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
+        return false;
+    }
+
+    // How many of the hand-offs an answer asked for are sent.
+    fn dm_handoffs_sent(asked: u32) -> u32 {
+        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
+        return asked;
+    }
+
+    test "a person calls an agent by tag, all tag, reply or its own topic" {
+        assert(addressed(true, false, false, false, false));
+        assert(addressed(false, true, false, false, false));
+        assert(addressed(false, false, true, false, false));
+        assert(addressed(false, false, false, true, false));
+        assert(!addressed(false, false, false, false, false));
+    }
+
+    test "a bot calls an agent only by tag or reply" {
+        assert(addressed(true, false, false, false, true));
+        assert(addressed(false, false, true, false, true));
+        assert(!addressed(false, true, false, false, true));
+        assert(!addressed(false, false, false, true, true));
+        assert(!addressed(false, true, false, true, true));
+    }
+
+    test "the bot chain closes after three bot messages" {
+        assert(chain_open(0));
+        assert(chain_open(2));
+        assert(!chain_open(3));
+        assert(!chain_open(4));
+    }
+
+    test "a bot waits out the cooldown before answering a bot" {
+        assert(!cooled(0));
+        assert(!cooled(4999));
+        assert(cooled(5000));
+    }
+
+    test "a person is always answered and a bot only inside the limits" {
+        assert(may_answer(false, 9, 0));
+        assert(may_answer(true, 2, 5000));
+        assert(!may_answer(true, 3, 5000));
+        assert(!may_answer(true, 2, 4999));
+    }
+
+    test "a creation claim lives fifteen minutes" {
+        assert(claim_fresh(0));
+        assert(claim_fresh(899999));
+        assert(!claim_fresh(900000));
+    }
+
+    test "typing is sent again four seconds after the last one" {
+        assert(!typing_due(0));
+        assert(!typing_due(3999));
+        assert(typing_due(4000));
+    }
+
+    test "on the all tag only the best fit answers at once" {
+        assert(speaks_on_all(0));
+        assert(!speaks_on_all(1));
+        assert(!speaks_on_all(7));
+    }
+
+    test "a person follows up in the same topic for two minutes without a tag" {
+        assert(follows_up(false, true, true, 0));
+        assert(follows_up(false, true, true, 119999));
+        assert(!follows_up(false, true, true, 120000));
+        assert(!follows_up(false, false, true, 0));
+        assert(!follows_up(false, true, false, 0));
+        assert(!follows_up(true, true, true, 0));
+    }
+
+    test "a stop silences bots for ten minutes and never a person" {
+        assert(!may_answer_after_stop(true, 0));
+        assert(!may_answer_after_stop(true, 599999));
+        assert(may_answer_after_stop(true, 600000));
+        assert(may_answer_after_stop(false, 0));
+    }
+
+    test "a muted agent still answers its own tag for the hour" {
+        assert(heard_while_muted(true, 0));
+        assert(!heard_while_muted(false, 0));
+        assert(!heard_while_muted(false, 3599999));
+        assert(heard_while_muted(false, 3600000));
+    }
+
+    test "the answer counts even when the chat refuses reactions" {
+        assert(turn_ok(true, false));
+        assert(turn_ok(true, true));
+        assert(!turn_ok(false, true));
+        assert(keeps_seen_mark(true));
+        assert(!keeps_seen_mark(false));
+    }
+
+    test "the topic brief wins over the group brief, and only an admin sets one" {
+        assert(brief_source(true, true) == 2);
+        assert(brief_source(true, false) == 2);
+        assert(brief_source(false, true) == 1);
+        assert(brief_source(false, false) == 0);
+        assert(brief_fits(0));
+        assert(brief_fits(1000));
+        assert(!brief_fits(1001));
+        assert(may_set_brief(true, false));
+        assert(!may_set_brief(false, false));
+        assert(!may_set_brief(true, true));
+    }
+
+    test "a deleted topic is recreated, at most once in ten minutes" {
+        assert(recreate_topic(false, false, 600000));
+        assert(recreate_topic(false, true, 600000));
+        assert(recreate_topic(true, false, 600000));
+        assert(!recreate_topic(true, true, 600000));
+        assert(!recreate_topic(true, false, 599999));
+    }
+
+    test "a summary reads fifty by default and never past the log" {
+        assert(summary_count(0) == 50);
+        assert(summary_count(1) == 1);
+        assert(summary_count(10) == 10);
+        assert(summary_count(200) == 200);
+        assert(summary_count(201) == 200);
+    }
+
+    test "in private a colleague is heard only with the hand-off tag" {
+        assert(dm_heard(false, false, false));
+        assert(dm_heard(true, true, true));
+        assert(!dm_heard(true, false, true));
+        assert(!dm_heard(true, true, false));
+    }
+
+    test "an ask runs a turn, an answer is only carried" {
+        assert(DM_ASK == 1);
+        assert(DM_ANSWER == 2);
+        assert(dm_runs_turn(DM_ASK));
+        assert(!dm_runs_turn(DM_ANSWER));
+        assert(!dm_runs_turn(0));
+        assert(dm_carries(DM_ANSWER));
+        assert(!dm_carries(DM_ASK));
+        assert(!dm_carries(0));
+    }
+
+    test "an answer is carried only while its ask is fresh" {
+        assert(DM_ANSWER_WINDOW_MS == 1800000);
+        assert(dm_tag_ok(DM_ASK, 4000000000));
+        assert(dm_tag_ok(DM_ANSWER, 0));
+        assert(dm_tag_ok(DM_ANSWER, 1799999));
+        assert(!dm_tag_ok(DM_ANSWER, 1800000));
+        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
+        assert(!dm_tag_ok(0, 0));
+        assert(!dm_tag_ok(3, 0));
+    }
+
+    test "an answer hands off to at most two colleagues" {
+        assert(MAX_DM_HANDOFFS == 2);
+        assert(dm_handoffs_sent(0) == 0);
+        assert(dm_handoffs_sent(1) == 1);
+        assert(dm_handoffs_sent(2) == 2);
+        assert(dm_handoffs_sent(3) == 2);
+        assert(dm_handoffs_sent(100) == 2);
+    }
+
+    test "VERSION says what the header's last note says" {
+        assert(VERSION == 4);
+    }
+}
+
+// phi^2 + 1/phi^2 = 3 | TRINITY
+

Открыть спеку урока в плеере ↗

+ +

Все уроки

+ +
+ + + diff --git a/apps/website/public/t27/files/specs/automation/agent-hive-group.t27 b/apps/website/public/t27/files/specs/automation/agent-hive-group.t27 index ab1bd5732a..2f801b0db7 100644 --- a/apps/website/public/t27/files/specs/automation/agent-hive-group.t27 +++ b/apps/website/public/t27/files/specs/automation/agent-hive-group.t27 @@ -12,6 +12,11 @@ // alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute. // v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs, // a topic a person deleted, and /summary. +// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private +// message to another bot by @username when Bot-to-Bot Communication Mode is on for both +// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the +// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn, +// and only while its ask is fresh. // Earlier laws are unchanged; each version only adds, so a host on an older one stays true. // phi^2 + 1/phi^2 = 3 | TRINITY @@ -19,7 +24,7 @@ module automation::agent_hive_group { pub const KIND : str = "automation"; pub const ID : str = "agent-hive-group"; pub const REPO : str = "999-multibots-telegraf"; - pub const VERSION : u8 = 3; + pub const VERSION : u8 = 4; // Bot messages in a row after one human message: human -> A -> B -> C, then silence. pub const MAX_BOT_DEPTH : u32 = 3; @@ -155,6 +160,47 @@ module automation::agent_hive_group { return asked; } + // ---- v4: hand-off in private, bot to bot ---- + + // Every private hand-off between two agents starts with this tag, then its kind and the + // person it is for. Only a hive agent of the same owner can send it, so the person named in + // it is trusted: the turn runs in that person's name, never the owner's. + pub const DM_TAG : str = "#a2a"; + pub const DM_ASK : u8 = 1; + pub const DM_ANSWER : u8 = 2; + // An answer to a person hands off to at most this many colleagues. + pub const MAX_DM_HANDOFFS : u32 = 2; + + // In private a person is always heard; a bot only when it is a colleague (an agent bot of + // the same owner) and its message carries the hand-off tag with a known kind. + fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool { + return !from_bot || (colleague && tag_ok); + } + + // An ask runs the colleague's turn; an answer is only carried to the person. So a private + // chain is two messages long and cannot loop, whatever the agents write. + fn dm_runs_turn(kind: u8) -> bool { + return kind == DM_ASK; + } + fn dm_carries(kind: u8) -> bool { + return kind == DM_ANSWER; + } + + // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never + // writes to a person nobody asked it about. Half an hour covers the longest turn. + pub const DM_ANSWER_WINDOW_MS : u32 = 1800000; + fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool { + if kind == DM_ASK { return true; } + if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; } + return false; + } + + // How many of the hand-offs an answer asked for are sent. + fn dm_handoffs_sent(asked: u32) -> u32 { + if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; } + return asked; + } + test "a person calls an agent by tag, all tag, reply or its own topic" { assert(addressed(true, false, false, false, false)); assert(addressed(false, true, false, false, false)); @@ -269,8 +315,46 @@ module automation::agent_hive_group { assert(summary_count(201) == 200); } + test "in private a colleague is heard only with the hand-off tag" { + assert(dm_heard(false, false, false)); + assert(dm_heard(true, true, true)); + assert(!dm_heard(true, false, true)); + assert(!dm_heard(true, true, false)); + } + + test "an ask runs a turn, an answer is only carried" { + assert(DM_ASK == 1); + assert(DM_ANSWER == 2); + assert(dm_runs_turn(DM_ASK)); + assert(!dm_runs_turn(DM_ANSWER)); + assert(!dm_runs_turn(0)); + assert(dm_carries(DM_ANSWER)); + assert(!dm_carries(DM_ASK)); + assert(!dm_carries(0)); + } + + test "an answer is carried only while its ask is fresh" { + assert(DM_ANSWER_WINDOW_MS == 1800000); + assert(dm_tag_ok(DM_ASK, 4000000000)); + assert(dm_tag_ok(DM_ANSWER, 0)); + assert(dm_tag_ok(DM_ANSWER, 1799999)); + assert(!dm_tag_ok(DM_ANSWER, 1800000)); + assert(!dm_tag_ok(DM_ANSWER, 4000000000)); + assert(!dm_tag_ok(0, 0)); + assert(!dm_tag_ok(3, 0)); + } + + test "an answer hands off to at most two colleagues" { + assert(MAX_DM_HANDOFFS == 2); + assert(dm_handoffs_sent(0) == 0); + assert(dm_handoffs_sent(1) == 1); + assert(dm_handoffs_sent(2) == 2); + assert(dm_handoffs_sent(3) == 2); + assert(dm_handoffs_sent(100) == 2); + } + test "VERSION says what the header's last note says" { - assert(VERSION == 3); + assert(VERSION == 4); } } diff --git a/apps/website/public/t27/files/specs/automation/review-jam.t27 b/apps/website/public/t27/files/specs/automation/review-jam.t27 index 9eeba322c6..80cd798a66 100644 --- a/apps/website/public/t27/files/specs/automation/review-jam.t27 +++ b/apps/website/public/t27/files/specs/automation/review-jam.t27 @@ -1,9 +1,11 @@ -// Review jam case detection and Queen action mapping -// Part of EPIC #5388 - gHashTag/t27#5700 +// Review jam: which case a stuck pull request is in, and the Queen's action. +// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700). +// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead +// of hand-copying them. module review_jam; -// Jam case constants (u8, position = value, starting at 0) +// Jam case constants (u8, value = position, starting at 0) pub const JAM_NONE: u8 = 0; pub const JAM_NO_REVIEWER: u8 = 1; pub const JAM_STACKED_ON_OPEN: u8 = 2; @@ -15,7 +17,7 @@ pub const JAM_NOISE_RED: u8 = 7; pub const JAM_DRAFT_OR_FRESH: u8 = 8; pub const JAM_UNMEASURED: u8 = 9; -// Queen action constants (u8, position = value, starting at 0) +// Queen action constants (u8, value = position, starting at 0) pub const ACT_NOTHING: u8 = 0; pub const ACT_DISPATCH_REVIEWER: u8 = 1; pub const ACT_WAIT_FOR_BASE: u8 = 2; @@ -27,18 +29,17 @@ pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7; pub const ACT_WAIT: u8 = 8; pub const ACT_MEASURE_AGAIN: u8 = 9; -// Determine the jam case based on PR state -// First matching rule wins, in this order: -// 1. not measured -> UNMEASURED -// 2. draft or fresh -> DRAFT_OR_FRESH -// 3. stacked on an open PR -> STACKED_ON_OPEN -// 4. conflicting -> CONFLICTING -// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED -// 6. refused and pushed after -> REFUSED_THEN_PUSHED -// 7. refused -> REFUSED_NOT_FIXED -// 8. only noise checks red -> NOISE_RED -// 9. no reviewer -> NO_REVIEWER -// 10. otherwise NONE +// The jam case of a pull request. The FIRST matching rule wins: +// 1. not measured -> UNMEASURED +// 2. draft or fresh -> DRAFT_OR_FRESH +// 3. stacked on an open PR -> STACKED_ON_OPEN +// 4. conflicting -> CONFLICTING +// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED +// 6. refused and pushed after -> REFUSED_THEN_PUSHED +// 7. refused -> REFUSED_NOT_FIXED +// 8. only noise checks red -> NOISE_RED +// 9. no reviewer -> NO_REVIEWER +// 10. otherwise -> NONE pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 { if !measured { return JAM_UNMEASURED; @@ -70,9 +71,9 @@ pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, con JAM_NONE } -// Map jam case to Queen action -// Case maps to action with same position (NONE -> NOTHING, NO_REVIEWER -> DISPATCH_REVIEWER, etc.) -// Any value above 9 -> ACT_MEASURE_AGAIN (unknown case is never "nothing to do") +// The Queen's action for a case: the action with the SAME position. +// Any value above 9 is an unknown case, and an unknown case is never +// "nothing to do": it maps to ACT_MEASURE_AGAIN. pub fn jam_action(case: u8) -> u8 { if case > 9 { return ACT_MEASURE_AGAIN; @@ -80,44 +81,48 @@ pub fn jam_action(case: u8) -> u8 { case } -// Determine if it's the Queen's turn to act -// True for every case except NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN +// Whether the case is the Queen's turn to act: true for every case except +// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN. pub fn jam_is_queens_turn(case: u8) -> bool { case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN } -// Tests - one per case, plus edge cases +// ---- jam_case: one test per case ---- test "unmeasured beats every other flag" { assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED; } +test "unmeasured with every other flag clear" { + assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED; +} + test "draft or fresh" { assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH; } test "stacked on open PR" { - assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN; + assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN; } test "conflicting" { - assert jam_case(true, false, false, true, false, false, false, false, false) == JAM_CONFLICTING; + assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING; } test "labeled not merged" { - assert jam_case(true, false, false, false, true, false, false, false, false) == JAM_LABELED_NOT_MERGED; + assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED; } test "refused then pushed" { - assert jam_case(true, false, false, false, false, true, true, false, false) == JAM_REFUSED_THEN_PUSHED; + assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED; } test "refused not fixed" { - assert jam_case(true, false, false, false, false, true, false, false, false) == JAM_REFUSED_NOT_FIXED; + assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED; } test "noise red" { - assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED; + assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED; } test "no reviewer" { @@ -128,70 +133,142 @@ test "none" { assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE; } -test "unknown case maps to measure again" { - assert jam_action(200) == ACT_MEASURE_AGAIN; -} +// ---- jam_case: first-match order, each rule against the next one ---- -test "queen's turn for labeled not merged" { - assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true; +test "draft beats stacked" { + assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH; } -test "queen's turn for no reviewer" { - assert jam_is_queens_turn(JAM_NO_REVIEWER) == true; +test "stacked and unreviewed is stacked, not no reviewer" { + assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN; } -test "queen's turn for conflicting" { - assert jam_is_queens_turn(JAM_CONFLICTING) == true; +test "stacked beats conflicting" { + assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN; } -test "queen's turn for refused then pushed" { - assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true; +test "conflicting beats labeled" { + assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING; } -test "queen's turn for refused not fixed" { - assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true; +test "labeled beats refused then pushed" { + assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED; } -test "queen's turn for noise red" { - assert jam_is_queens_turn(JAM_NOISE_RED) == true; +test "refused then pushed beats noise red" { + assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED; } -test "queen's turn for unmeasured" { - assert jam_is_queens_turn(JAM_UNMEASURED) == true; +test "refused not fixed beats noise red" { + assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED; } -test "not queen's turn for none" { - assert jam_is_queens_turn(JAM_NONE) == false; +test "noise red beats no reviewer" { + assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED; } -test "not queen's turn for draft or fresh" { - assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false; +test "pushed without a refusal is not refused then pushed" { + assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE; } -test "not queen's turn for stacked on open" { - assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false; +// ---- jam_action: same position, unknown -> measure again ---- + +test "action none to nothing" { + assert jam_action(JAM_NONE) == ACT_NOTHING; } -test "specific scenario: labeled reviewed but still open" { - assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED; +test "action no reviewer to dispatch reviewer" { + assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER; } -test "specific scenario: unmeasured with any other flags" { - assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED; +test "action stacked to wait for base" { + assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE; } -test "action mapping: none to nothing" { - assert jam_action(JAM_NONE) == ACT_NOTHING; +test "action refused not fixed to dispatch coder" { + assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER; } -test "action mapping: no reviewer to dispatch reviewer" { - assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER; +test "action refused then pushed to dispatch re-review" { + assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW; } -test "action mapping: labeled not merged to finish merge" { +test "action labeled not merged to finish merge" { assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE; } -test "action mapping: unmeasured to measure again" { +test "action conflicting to rebase by coder" { + assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER; +} + +test "action noise red to name noise and review" { + assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW; +} + +test "action draft or fresh to wait" { + assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT; +} + +test "action unmeasured to measure again" { assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN; -} \ No newline at end of file +} + +test "action first unknown case measures again" { + assert jam_action(10) == ACT_MEASURE_AGAIN; +} + +test "action unknown case 200 measures again" { + assert jam_action(200) == ACT_MEASURE_AGAIN; +} + +// ---- jam_is_queens_turn ---- + +test "queens turn for no reviewer" { + assert jam_is_queens_turn(JAM_NO_REVIEWER) == true; +} + +test "queens turn for refused not fixed" { + assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true; +} + +test "queens turn for refused then pushed" { + assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true; +} + +test "queens turn for labeled not merged" { + assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true; +} + +test "queens turn for conflicting" { + assert jam_is_queens_turn(JAM_CONFLICTING) == true; +} + +test "queens turn for noise red" { + assert jam_is_queens_turn(JAM_NOISE_RED) == true; +} + +test "queens turn for unmeasured" { + assert jam_is_queens_turn(JAM_UNMEASURED) == true; +} + +test "not queens turn for none" { + assert jam_is_queens_turn(JAM_NONE) == false; +} + +test "not queens turn for draft or fresh" { + assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false; +} + +test "not queens turn for stacked on open" { + assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false; +} + +// ---- the two scenarios of #9032, end to end ---- + +test "scenario bee-reviewed but still open finishes the merge" { + assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE; +} + +test "scenario failed read is never reported as free" { + assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN; +} diff --git a/apps/website/public/t27/files/specs/queen/dispatch.t27 b/apps/website/public/t27/files/specs/queen/dispatch.t27 index e49055757e..42911bfbb6 100644 --- a/apps/website/public/t27/files/specs/queen/dispatch.t27 +++ b/apps/website/public/t27/files/specs/queen/dispatch.t27 @@ -137,8 +137,35 @@ pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a d ; --- Events, persistence, idempotency, projections -------------------------------------------------- pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)"; pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round"; -pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, and for a finished row verdict and judgedHead", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"]; +pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"]; pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board"; + +; --- End reasons for runs that produce no commits ----------------------------------------------------------- +; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted. +; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days. +pub const R_MODEL_ENDED_TURN : u32 = 0; // Model ended its turn without editing any files +pub const R_TIME_TOKEN_LIMIT : u32 = 1; // Hit time or token limit during execution +pub const R_CRASHED : u32 = 2; // Container or tool crashed (with exit code) +pub const R_BOUNDARY_CORRECT : u32 = 3; // Boundary file was already correct, no changes needed +pub const R_REFUSAL_START : u32 = 4; // Refusal before the run started (invalid task, missing resources) +pub const R_PUSH_FAILED : u32 = 5; // Push to remote failed after successful run +pub const R_UNKNOWN : u32 = 6; // Cause could not be determined by the runner + +pub const END_REASON_COUNT : u32 = 7; +pub const END_REASON_NAMES : [7]str = [ + "model_ended_turn", + "time_token_limit", + "crashed", + "boundary_correct", + "refusal_start", + "push_failed", + "unknown" +]; + +; --- Public board extension: show end reason on empty cards ----------------------------------------------- +; The public board's "empty" cards now include the end reason when available, enabling breakdown of +; the 56.0% empty-branch share by reason from public endpoints alone. +pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available"; pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints"; ; --- Agents and workers --------------------------------------------------------------------------------- pub const AGENT_LETTERS : u32 = 27; @@ -317,6 +344,42 @@ invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; } invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; } invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; } invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; } +invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); } +invariant all_end_reasons_valid { + assert is_valid_end_reason(R_MODEL_ENDED_TURN); + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT); + assert is_valid_end_reason(R_CRASHED); + assert is_valid_end_reason(R_BOUNDARY_CORRECT); + assert is_valid_end_reason(R_REFUSAL_START); + assert is_valid_end_reason(R_PUSH_FAILED); + assert is_valid_end_reason(R_UNKNOWN); +} + +; --- End reason handling functions -------------------------------------------------------------------------- + +pub fn is_valid_end_reason(reason: u32) -> bool { + return reason < END_REASON_COUNT; +} + +pub fn end_reason_to_name(reason: u32) -> str { + if (is_valid_end_reason(reason)) { + return END_REASON_NAMES[reason]; + } + return "invalid_end_reason"; +} + +pub fn is_no_diff_end_reason(reason: u32) -> bool { + return reason == R_MODEL_ENDED_TURN + or reason == R_TIME_TOKEN_LIMIT + or reason == R_BOUNDARY_CORRECT + or reason == R_REFUSAL_START + or reason == R_PUSH_FAILED + or reason == R_UNKNOWN; +} + +pub fn is_crash_end_reason(reason: u32) -> bool { + return reason == R_CRASHED; +} test the_table_moves_only_along_its_guards { assert next_state(S_NONE, E_START) == S_RUNNING; @@ -329,6 +392,28 @@ test the_table_moves_only_along_its_guards { assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL; assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING; assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL; + // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886). + assert next_state(S_NONE, E_REFUSE) == S_REFUSED; + assert next_state(S_NONE, E_END) == ILLEGAL; + assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED; + assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED; + assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK; + assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE; + assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT; + assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY; + assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK; + assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_START) == ILLEGAL; + assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING; } test a_claim_follows_the_verdict_and_three_clocks { @@ -341,6 +426,16 @@ test a_claim_follows_the_verdict_and_three_clocks { assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED; assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED; assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW; + // #9886: each verdict that frees the issue, the send-back floor at its edge with a release + // already spent, and the wait floor's two edges. + assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED; + assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW; + assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW; } test a_closed_issue_is_drawn_done_whatever_its_verdict { @@ -348,12 +443,16 @@ test a_closed_issue_is_drawn_done_whatever_its_verdict { assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW; assert column_of(C_RUNNING, false, true) == COL_RUNNING; assert column_of(C_FAILED, true, true) == COL_DROPPED; + assert column_of(C_ACCEPTED, true, true) == COL_DONE; } test the_first_eligible_wins_in_any_order { assert chosen_of(false, false, true, true, false) == 2; assert chosen_of(false, true, false, false, true) == 1; assert chosen_of(false, false, false, false, false) == NONE_CHOSEN; + assert chosen_of(true, true, false, false, false) == 0; + assert chosen_of(false, false, false, true, true) == 3; + assert chosen_of(false, false, false, false, true) == 4; assert can_start(3, DEFAULT_WORKER_LIMIT) == true; assert can_start(4, DEFAULT_WORKER_LIMIT) == false; } @@ -369,6 +468,7 @@ test review_escalates_without_work_and_after_two_send_backs { assert retry_allowed(1) == true; assert retry_allowed(2) == false; assert counts_against_issue(true) == false; + assert counts_against_issue(false) == true; } test one_lease_one_owner_and_age_alone_reaps { @@ -379,7 +479,116 @@ test one_lease_one_owner_and_age_alone_reaps { assert owners_per_term() == 1; assert stall_reapable(121) == true; assert stall_reapable(119) == false; + // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself. + assert stall_reapable(120) == false; assert followup_rounds(5) == 1; + assert followup_rounds(0) == 0; assert accept_requires_pull_request() == false; assert writer_enforces_table() == false; } + +; --- End reason tests -------------------------------------------------------------------------------------- + +test end_reason_constants_are_correct { + assert R_MODEL_ENDED_TURN == 0; + assert R_TIME_TOKEN_LIMIT == 1; + assert R_CRASHED == 2; + assert R_BOUNDARY_CORRECT == 3; + assert R_REFUSAL_START == 4; + assert R_PUSH_FAILED == 5; + assert R_UNKNOWN == 6; + assert END_REASON_COUNT == 7; + // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two + // call the functions that read the constants: the last reason is named, one past it is not. + assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown"; + assert is_valid_end_reason(END_REASON_COUNT) == false; +} + +test end_reason_names_array_matches_constants { + assert END_REASON_NAMES[0] == "model_ended_turn"; + assert END_REASON_NAMES[1] == "time_token_limit"; + assert END_REASON_NAMES[2] == "crashed"; + assert END_REASON_NAMES[3] == "boundary_correct"; + assert END_REASON_NAMES[4] == "refusal_start"; + assert END_REASON_NAMES[5] == "push_failed"; + assert END_REASON_NAMES[6] == "unknown"; +} + +test end_reason_validation_works { + assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_valid_end_reason(R_CRASHED) == true; + assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_valid_end_reason(R_REFUSAL_START) == true; + assert is_valid_end_reason(R_PUSH_FAILED) == true; + assert is_valid_end_reason(R_UNKNOWN) == true; + assert is_valid_end_reason(7) == false; // One past the last valid reason + assert is_valid_end_reason(99) == false; // Way past the last valid reason +} + +test end_reason_to_name_conversion { + assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn"; + assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit"; + assert end_reason_to_name(R_CRASHED) == "crashed"; + assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct"; + assert end_reason_to_name(R_REFUSAL_START) == "refusal_start"; + assert end_reason_to_name(R_PUSH_FAILED) == "push_failed"; + assert end_reason_to_name(R_UNKNOWN) == "unknown"; + assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string +} + +test no_diff_end_reason_classification { + assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_no_diff_end_reason(R_REFUSAL_START) == true; + assert is_no_diff_end_reason(R_PUSH_FAILED) == true; + assert is_no_diff_end_reason(R_UNKNOWN) == true; + assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category +} + +test crash_end_reason_classification { + assert is_crash_end_reason(R_CRASHED) == true; + assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false; + assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false; + assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false; + assert is_crash_end_reason(R_REFUSAL_START) == false; + assert is_crash_end_reason(R_PUSH_FAILED) == false; + assert is_crash_end_reason(R_UNKNOWN) == false; +} + +test end_reason_enables_breakdown_of_empty_branches { + // This test validates that the end reason vocabulary enables breaking down + // the 56.0% empty-branch share by reason from public endpoints alone + + // Simulate counts for empty branches by reason + var model_ended_count : u32 = 100; + var token_limit_count : u32 = 80; + var boundary_correct_count : u32 = 60; + var refusal_start_count : u32 = 40; + var push_failed_count : u32 = 30; + var unknown_count : u32 = 7; // Should be < 10% of total + var crash_count : u32 = 0; + + var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count + crash_count; + + // Verify unknown is less than 10% of total (success criterion from issue) + assert unknown_count < total_empty / 10; + + // Verify we can calculate breakdown percentages + var model_percentage : u32 = (model_ended_count * 100) / total_empty; + var token_percentage : u32 = (token_limit_count * 100) / total_empty; + var unknown_percentage : u32 = (unknown_count * 100) / total_empty; + + assert model_percentage > 0; + assert token_percentage > 0; + assert unknown_percentage < 10; // Unknown should be < 10% + + // Verify no-diff vs crash categorization works + var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count; + var crash_total : u32 = crash_count; + + assert no_diff_total == total_empty - crash_total; +} diff --git a/apps/website/public/t27/files/specs/queen/drain.t27 b/apps/website/public/t27/files/specs/queen/drain.t27 new file mode 100644 index 0000000000..8a5ceee5bd --- /dev/null +++ b/apps/website/public/t27/files/specs/queen/drain.t27 @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/drain.t27 -- when a draining Queen process may exit: what it waits for, what it stops, what it leaves +; Owner's night loop, 2026-10-10 (gHashTag/trios#1729 item 2): "every deploy still freezes the swarm for +; 1800 s; with durable jobs and waits and stoppable turns, a spec can bound the drain by in-flight turns". +; Measured before this card (gHashTag/trios trios/agent-server, branch queen): +; - the drain is not a fixed wait. On SIGTERM the server drains its own bee runner (queen-runner.ts +; drainBeeRunner): it takes no new order and waits while any bee it carries still runs, up to +; TRIOS_BEE_RUNNER_DRAIN_SECONDS = 1800, then salvages the branch of each bee still running and exits +; about 2 s later. Rounds, the reviewer and the wait scheduler run on through that wait +; (roundsThroughDrain, BrowserOS#546) and are cut by the exit. Railway's +; RAILWAY_DEPLOYMENT_DRAINING_SECONDS = 1830 is only the SIGKILL that follows; the service has a +; volume, so the next container starts when this one ends. +; - so the Queen's own bees are what the drain waits for, and they usually outlive it. GitHub's +; deployment statuses for the Queen's service, 2026-10-08 23:14Z to 2026-10-10 12:01Z: 7 of 8 deploys +; took 30.4 to 33.7 min from start to live, and one took 2.8 min (00:44Z, a quiet night). +; - the 1800 s protects a bee that ends within 30 min of SIGTERM. A bee still running then is cut +; anyway (salvaged, its conversation lost), and its row stays claimed until its task lease lapses. +; Owner rules this card keeps (2026-10-08): a deploy must not cut a bee short, and must not idle the +; swarm; never shorten the drain without asking (a 120 s drain was tried and reverted in 5 min). +; How others stop for a deploy: OTP stops children in reverse order with a shutdown time each, then +; kills; Akka Cluster Sharding hands a shard off within 60 s and warns that Kubernetes' 30 s grace can +; be shorter; Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds, then SIGKILL; DBOS, +; Temporal and Restate keep a workflow's progress in the store, so a stopped process loses only the +; step it was on. +; What is reused, not restated: +; - actors.t27 turn_signal (a turn's own bound, 0 meaning TURN_MAX_SECONDS) and stop_signal +; (X_SHUTDOWN, then X_KILL past a timeout); +; - turn_stop.t27: a stopped turn gets its abort, TURN_STOP_GRACE_MS, then its escalation, and its lane +; is free within LANE_FREE_MS; +; - waits.t27 section 7: a deploy writes nothing to a wait; the next process takes it up +; (pickup_after_seconds); +; - jobs.t27: a job holds no lane, and an effect runs through the effects journal, so a process that +; stops mid-step never repeats it; +; - control.t27 section 2: a runner's task lease lapses TASK_LEASE_TTL_SECONDS after its last renewal, +; and only then is its task reclaimable. +; Not here: the store, the signals, the salvage (the runtime, gHashTag/trios). +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenDrain; + +use queen::actors::turn_signal; +use queen::actors::stop_signal; +use queen::control::reclaimable; +use queen::waits::pickup_after_seconds; + +; --- 1. What a draining process holds --------------------------------------------------------------- + +/// The kinds of work a Queen process can hold when SIGTERM arrives. +/// WK_BEE: a bee this process runs for its runner (queen-runner.ts), a turn of up to an hour. +/// WK_REVIEW: one review (reviewer.t27), a turn bounded by REVIEW_ROW_SECONDS. +/// WK_ROUND: a Queen round, a reconcile: it re-reads the store and decides again. +/// WK_JOB: a step of a job (jobs.t27). +/// WK_WAIT: a wait row this process claimed (waits.t27). +pub const WK_BEE : u8 = 0; +pub const WK_REVIEW : u8 = 1; +pub const WK_ROUND : u8 = 2; +pub const WK_JOB : u8 = 3; +pub const WK_WAIT : u8 = 4; +pub const WORK_KINDS : u8 = 5; + +/// actors.t27 X_KILL and X_NONE, written as literals here (stop_signal and turn_signal answer them). +pub const X_KILL_LIT : u8 = 2; +pub const X_NONE_LIT : u8 = 99; + +/// Whether the store keeps this work as it stands, so another process continues it from there and +/// a stop throws nothing away. A wait is a row a deploy never writes (waits.t27 section 7). A job is +/// a row and its effects are journaled (jobs.t27). A round is a reconcile: everything it decided is +/// decided again from the store by the next round, and every effect it made is journaled. A review +/// and a bee are turns: their row stays, but what the turn has done so far is in its process. +pub fn row_keeps(kind: u8) -> bool { + if (kind == WK_WAIT) { return true; } + if (kind == WK_JOB) { return true; } + if (kind == WK_ROUND) { return true; } + return false; +} + +/// A turn's own bound in seconds, as actors.t27 turn_signal reads it (0 is TURN_MAX_SECONDS, 3600). +/// A review is reviewer.t27 REVIEW_ROW_SECONDS (300, a literal here). A bee names none. +pub const REVIEW_BOUND_SECONDS : u32 = 300; + +pub fn bound_of(kind: u8) -> u32 { + if (kind == WK_REVIEW) { return REVIEW_BOUND_SECONDS; } + return 0; +} + +/// The most work a drain may throw away to end sooner: one review. A turn whose own timer kills it +/// within this many seconds (turn_signal says X_KILL at this age) loses at most that much to a stop, +/// and the next process redoes it from its row. A bee's turn does not qualify: its bound is an hour, +/// and what it has done lives in its conversation, which no row keeps. Chosen, not measured. +pub const STOP_LOSS_SECONDS : u32 = 300; + +pub fn redo_is_cheap(bound_seconds: u32) -> bool { + return turn_signal(STOP_LOSS_SECONDS, bound_seconds) == X_KILL_LIT; +} + +; --- 2. What the drain owes each piece of work -------------------------------------------------------- + +/// DV_DONE: its work ended; nothing is owed. +/// DV_LEAVE: the store keeps it (row_keeps); the process exits without it and the next one takes it up. +/// DV_STOP: the drain does not wait for it. It runs on while something else holds the process, and is +/// stopped when the process exits, through turn_stop.t27 (its abort, the grace, the escalation). +/// DV_HOLD: the drain waits for it to end on its own. +/// DV_HAND_BACK: held to the cap and still running: it is stopped as DV_STOP is, what it wrote is +/// salvaged into its branch and stored, and its row is ended now, so its issue is free at once. +pub const DV_DONE : u8 = 0; +pub const DV_LEAVE : u8 = 1; +pub const DV_STOP : u8 = 2; +pub const DV_HOLD : u8 = 3; +pub const DV_HAND_BACK : u8 = 4; + +/// A bee is held until it ends or the cap passes: the owner's rule that a deploy does not cut a bee +/// short, kept to the same cap as before this card. A review is not held: a stop loses at most one +/// review, which the next process redoes. Rows are left. +pub fn drain_verdict(kind: u8, ended: bool, waited_seconds: u32, cap_seconds: u32) -> u8 { + if (ended) { return DV_DONE; } + if (row_keeps(kind)) { return DV_LEAVE; } + if (redo_is_cheap(bound_of(kind))) { return DV_STOP; } + if (waited_seconds >= cap_seconds) { return DV_HAND_BACK; } + return DV_HOLD; +} + +; --- 3. What keeps working while something holds ------------------------------------------------------ + +/// Whether a source of work starts anything new during the drain. Nothing that would start a bee in +/// this process: that bee would hold the drain for up to its hour. Everything else keeps going while +/// a held bee keeps the process alive, because the next process cannot start before this one ends +/// (one instance, one volume), and stopping the rounds then idled the swarm for the whole drain +/// (measured 2026-10-08, BrowserOS#546). Once nothing holds, the process is leaving: nothing starts. +pub fn takes_new_work(starts_a_bee_here: bool, holding: u32) -> bool { + if (starts_a_bee_here) { return false; } + return holding > 0; +} + +; --- 4. The hard cap ---------------------------------------------------------------------------------- + +/// What must still fit after the cap, before the platform's SIGKILL: the hand-back of every held bee +/// (its abort and grace, the salvage into its branch, the bundle, the row), run side by side under one +/// deadline, then the server's own close (main.ts gives it 2 s). Chosen, not measured. +pub const HAND_BACK_SECONDS : u32 = 20; +pub const EXIT_SECONDS : u32 = 5; + +/// The cap: the runner's drain (TRIOS_BEE_RUNNER_DRAIN_SECONDS), never so late that the hand-back and +/// the exit cannot finish before the platform kills the process (RAILWAY_DEPLOYMENT_DRAINING_SECONDS). +/// A platform drain of 0 is one this process cannot read: the runner's drain stands, as before. +/// With today's 1800 and 1830 the cap is 1800: this card cuts no bee sooner than the drain it replaces. +pub fn drain_cap_seconds(runner_drain_seconds: u32, platform_seconds: u32) -> u32 { + if (platform_seconds == 0) { return runner_drain_seconds; } + if (platform_seconds <= HAND_BACK_SECONDS + EXIT_SECONDS) { return 0; } + let latest : u32 = platform_seconds - HAND_BACK_SECONDS - EXIT_SECONDS; + if (runner_drain_seconds < latest) { return runner_drain_seconds; } + return latest; +} + +; --- 5. The exit -------------------------------------------------------------------------------------- + +/// turn_stop.t27's promises, written as literals here: the grace after the abort, and the time by +/// which a stopped turn's lane is free. +pub const TURN_STOP_GRACE_MS : u32 = 3000; +pub const LANE_FREE_MS : u32 = 5000; + +pub const EXIT_NOW : u8 = 0; +pub const EXIT_WAIT : u8 = 1; + +/// The process exits once nothing holds it and every turn it stopped has had its stop land: its abort +/// at once, the grace, the escalation (stop_signal with the grace says X_KILL by LANE_FREE_MS). A +/// process that stopped nothing exits at once. +pub fn exit_step(holding: u32, stopping: u32, since_stop_ms: u32) -> u8 { + if (holding > 0) { return EXIT_WAIT; } + if (stopping == 0) { return EXIT_NOW; } + if (since_stop_ms >= LANE_FREE_MS) { return EXIT_NOW; } + return EXIT_WAIT; +} + +; --- 6. When the work is free for another process ----------------------------------------------------- + +/// control.t27 TASK_LEASE_TTL_SECONDS, written as a literal here (pinned by the invariant below). +pub const TASK_LEASE_TTL_LIT : u32 = 180; + +/// Seconds after the exit before another process may take the issue of a bee the drain stopped. +/// Handed back, its row is ended at the cap: free at once. Left claimed, as before this card, it waits +/// until its task lease lapses (control.t27 reclaimable), counted from the last renewal, which stopped +/// with the process. +pub fn bee_free_after_seconds(handed_back: bool, since_renewal: u32) -> u32 { + if (handed_back) { return 0; } + if (reclaimable(false, since_renewal)) { return 0; } + return TASK_LEASE_TTL_LIT - since_renewal; +} + +/// Seconds after the exit before another process may take a wait row this process had claimed: the +/// rest of its claim (waits.t27 pickup_after_seconds). The drain writes nothing to it. +pub fn wait_free_after_seconds(claimed: bool, since_claim: u32) -> u32 { + return pickup_after_seconds(claimed, since_claim); +} + +; --- tests -------------------------------------------------------------------------------------------- + +test rows_are_left_to_the_next_process { + assert row_keeps(WK_WAIT) == true; + assert row_keeps(WK_JOB) == true; + assert row_keeps(WK_ROUND) == true; + assert row_keeps(WK_REVIEW) == false; + assert row_keeps(WK_BEE) == false; + assert drain_verdict(WK_WAIT, false, 0, 1800) == DV_LEAVE; + assert drain_verdict(WK_JOB, false, 0, 1800) == DV_LEAVE; + assert drain_verdict(WK_ROUND, false, 0, 1800) == DV_LEAVE; + // past the cap too: a row is never handed back, it is simply there + assert drain_verdict(WK_WAIT, false, 1800, 1800) == DV_LEAVE; +} + +test a_review_is_stopped_at_the_exit_not_waited_for { + assert bound_of(WK_REVIEW) == 300; + assert redo_is_cheap(300) == true; + assert redo_is_cheap(301) == false; + assert drain_verdict(WK_REVIEW, false, 0, 1800) == DV_STOP; + assert drain_verdict(WK_REVIEW, false, 1800, 1800) == DV_STOP; +} + +test a_bee_is_held_until_it_ends_or_the_cap { + assert bound_of(WK_BEE) == 0; + // its bound is TURN_MAX_SECONDS: a stop could throw away an hour + assert redo_is_cheap(0) == false; + assert redo_is_cheap(3600) == false; + assert drain_verdict(WK_BEE, false, 0, 1800) == DV_HOLD; + assert drain_verdict(WK_BEE, false, 1799, 1800) == DV_HOLD; + assert drain_verdict(WK_BEE, false, 1800, 1800) == DV_HAND_BACK; + assert drain_verdict(WK_BEE, false, 4000, 1800) == DV_HAND_BACK; + // a bee that ended is owed nothing, at any time + assert drain_verdict(WK_BEE, true, 0, 1800) == DV_DONE; + assert drain_verdict(WK_BEE, true, 1800, 1800) == DV_DONE; + assert drain_verdict(WK_REVIEW, true, 0, 1800) == DV_DONE; + assert drain_verdict(WK_WAIT, true, 0, 1800) == DV_DONE; +} + +test a_cap_of_zero_hands_every_bee_back_at_once { + assert drain_verdict(WK_BEE, false, 0, 0) == DV_HAND_BACK; +} + +test nothing_starts_a_bee_here_and_the_rest_runs_only_while_held { + assert takes_new_work(true, 3) == false; + assert takes_new_work(true, 0) == false; + assert takes_new_work(false, 1) == true; + assert takes_new_work(false, 0) == false; +} + +test the_cap_is_todays_drain_and_fits_the_platform { + // today: runner 1800, Railway 1830 -> 1800, no bee cut sooner than before + assert drain_cap_seconds(1800, 1830) == 1800; + // a platform drain too short for the hand-back moves the cap earlier + assert drain_cap_seconds(1800, 1800) == 1775; + assert drain_cap_seconds(1800, 26) == 1; + assert drain_cap_seconds(1800, 25) == 0; + assert drain_cap_seconds(1800, 10) == 0; + // a shorter runner drain is the owner's to choose, and it stands + assert drain_cap_seconds(600, 1830) == 600; + assert drain_cap_seconds(0, 1830) == 0; + // a platform drain this process cannot read leaves the runner's drain alone + assert drain_cap_seconds(1800, 0) == 1800; +} + +test the_exit_waits_for_held_work_and_for_stops_to_land { + assert exit_step(1, 0, 0) == EXIT_WAIT; + assert exit_step(2, 3, 9000) == EXIT_WAIT; + assert exit_step(0, 0, 0) == EXIT_NOW; + assert exit_step(0, 2, 0) == EXIT_WAIT; + assert exit_step(0, 2, 4999) == EXIT_WAIT; + assert exit_step(0, 2, 5000) == EXIT_NOW; +} + +test a_handed_back_bee_is_free_at_once_and_a_left_one_waits_out_its_lease { + assert bee_free_after_seconds(true, 0) == 0; + assert bee_free_after_seconds(true, 100) == 0; + assert bee_free_after_seconds(false, 0) == 180; + assert bee_free_after_seconds(false, 45) == 135; + assert bee_free_after_seconds(false, 179) == 1; + assert bee_free_after_seconds(false, 180) == 0; + assert bee_free_after_seconds(false, 4294967295) == 0; +} + +test a_wait_row_is_free_after_the_rest_of_its_claim { + assert wait_free_after_seconds(false, 0) == 0; + assert wait_free_after_seconds(true, 0) == 60; + assert wait_free_after_seconds(true, 45) == 15; + assert wait_free_after_seconds(true, 60) == 0; +} + +invariant the_verdicts_and_kinds_are_pinned { + assert DV_DONE == 0; + assert DV_LEAVE == 1; + assert DV_STOP == 2; + assert DV_HOLD == 3; + assert DV_HAND_BACK == 4; + assert WK_BEE == 0; + assert WK_WAIT == 4; + assert WORK_KINDS == 5; + assert EXIT_NOW == 0; + assert EXIT_WAIT == 1; +} + +invariant the_literals_match_the_cards_they_come_from { + // actors.t27: turn_signal answers X_KILL at the bound and X_NONE before it + assert turn_signal(300, 300) == X_KILL_LIT; + assert turn_signal(299, 300) == X_NONE_LIT; + // control.t27: the task lease lapses at TASK_LEASE_TTL_SECONDS + assert reclaimable(false, TASK_LEASE_TTL_LIT) == true; + assert reclaimable(false, TASK_LEASE_TTL_LIT - 1) == false; + // turn_stop.t27: by LANE_FREE_MS a stopped turn's grace has passed (stop_signal says X_KILL) + assert stop_signal(LANE_FREE_MS, TURN_STOP_GRACE_MS) == X_KILL_LIT; + assert stop_signal(TURN_STOP_GRACE_MS - 1, TURN_STOP_GRACE_MS) == X_NONE_LIT; +} + +invariant a_review_is_cheap_to_redo_and_a_bee_is_not { + assert STOP_LOSS_SECONDS == REVIEW_BOUND_SECONDS; + assert redo_is_cheap(REVIEW_BOUND_SECONDS) == true; + assert redo_is_cheap(0) == false; +} + +invariant the_hand_back_fits_between_todays_drain_and_the_platform_kill { + assert HAND_BACK_SECONDS + EXIT_SECONDS <= 30; + assert drain_cap_seconds(1800, 1830) == 1800; +} diff --git a/apps/website/public/t27/files/specs/queen/hive.t27 b/apps/website/public/t27/files/specs/queen/hive.t27 new file mode 100644 index 0000000000..ec636503bf --- /dev/null +++ b/apps/website/public/t27/files/specs/queen/hive.t27 @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to +; a cell, how a ship leaves, and how often the map may grow +; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game +; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4. +; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day): +; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen. +; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far +; from the repository it works for. +; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole +; scene: one rebuild for most bees that started. +; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same +; as no news. +; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two +; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenHive; + +pub const HIVE_KINDS : u8 = 10; + +; --- 1. Where a ship flies --------------------------------------------------------------------------- + +/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this +/// map can show (a repository with no region, or a task that is not an issue). +pub const CELL_PLACED : u8 = 0; +pub const CELL_PENDING : u8 = 1; +pub const CELL_NONE : u8 = 2; + +/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim, +/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is +/// placed. HOME: flying back to the entrance, its task over. +pub const SM_PARK : u8 = 0; +pub const SM_ORBIT : u8 = 1; +pub const SM_HOVER : u8 = 2; +pub const SM_PORTAL : u8 = 3; +pub const SM_HOME : u8 = 4; + +/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no +/// longer in the feed. +pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 { + if (departing) { return SM_HOME; } + if (queued) { return SM_PARK; } + if (cell == CELL_PENDING) { return SM_PORTAL; } + if (cell != CELL_PLACED) { return SM_PARK; } + if (working) { return SM_ORBIT; } + return SM_HOVER; +} + +; --- 2. How a ship leaves ---------------------------------------------------------------------------- + +/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing. +pub const DEPART_MS : u32 = 2400; + +pub fn ship_kept(ms_since_left: u32) -> bool { + return ms_since_left < DEPART_MS; +} + +/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing. +pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 { + if (departing == false) { return 1000; } + if (ms_since_left >= DEPART_MS) { return 0; } + return 1000 - (ms_since_left * 1000) / DEPART_MS; +} + +; --- 3. What an event does to a cell ----------------------------------------------------------------- + +/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the +/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is +/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it. +pub const FX_NONE : u8 = 0; +pub const FX_START : u8 = 1; +pub const FX_ACCEPT : u8 = 2; +pub const FX_SEND_BACK : u8 = 3; +pub const FX_LOST : u8 = 4; +pub const FX_CANCEL : u8 = 5; + +/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by +/// its verdict, cancel CANCEL; everything else nothing. +pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0]; + +/// `accepted`: a review's verdict was an acceptance. Only a review reads it. +pub fn cell_effect(kind: u8, accepted: bool) -> u8 { + if (kind >= HIVE_KINDS) { return FX_NONE; } + let fx : u8 = FX_OF_KIND[kind]; + if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; } + return fx; +} + +/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn. +pub const FLASH_MS : u32 = 1800; + +pub fn flash_alpha(ms_since: u32) -> u32 { + if (ms_since >= FLASH_MS) { return 0; } + return 1000 - (ms_since * 1000) / FLASH_MS; +} + +; --- 4. How often the map may grow ------------------------------------------------------------------- + +/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding +/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS -- +/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of +/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's +/// portal (SM_PORTAL), so where it is going is already true. +pub const CELL_BATCH_SECONDS : u32 = 300; + +pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool { + if (pending == 0) { return false; } + if (placed_before == false) { return true; } + return seconds_since_batch >= CELL_BATCH_SECONDS; +} + +test a_ship_flies_where_its_bee_is { + // queued: at the entrance, wherever its cell stands + assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK; + assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK; + // claimed, its cell placed: orbit while heard, hover when quiet + assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT; + assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER; + // its cell not placed yet: at its repository's portal, working or not + assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL; + assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL; + // no cell this map can show: back at the entrance + assert ship_mode(false, true, CELL_NONE, false) == SM_PARK; + // gone from the feed: home, whatever it was + assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME; + assert ship_mode(true, false, CELL_NONE, true) == SM_HOME; +} + +test a_ship_leaves_by_flying_home { + assert ship_kept(0) == true; + assert ship_kept(DEPART_MS - 1) == true; + assert ship_kept(DEPART_MS) == false; + assert ship_alpha(false, 0) == 1000; + assert ship_alpha(false, 99999) == 1000; + assert ship_alpha(true, 0) == 1000; + assert ship_alpha(true, 1200) == 500; + assert ship_alpha(true, DEPART_MS - 1) == 1; + assert ship_alpha(true, DEPART_MS) == 0; + assert ship_alpha(true, 4000000000) == 0; +} + +test an_event_lights_its_cell { + assert cell_effect(0, false) == FX_START; + assert cell_effect(1, true) == FX_NONE; + assert cell_effect(2, false) == FX_NONE; + assert cell_effect(3, false) == FX_LOST; + assert cell_effect(4, true) == FX_NONE; + assert cell_effect(5, true) == FX_ACCEPT; + assert cell_effect(5, false) == FX_SEND_BACK; + assert cell_effect(6, true) == FX_NONE; + assert cell_effect(7, false) == FX_CANCEL; + assert cell_effect(8, false) == FX_NONE; + assert cell_effect(9, true) == FX_NONE; + // only a review reads the verdict + assert cell_effect(3, true) == FX_LOST; + assert cell_effect(HIVE_KINDS, true) == FX_NONE; +} + +test a_flash_fades { + assert flash_alpha(0) == 1000; + assert flash_alpha(900) == 500; + assert flash_alpha(FLASH_MS - 1) == 1; + assert flash_alpha(FLASH_MS) == 0; + assert flash_alpha(4000000000) == 0; +} + +test the_map_grows_in_batches { + assert cells_due(0, false, 0) == false; + assert cells_due(0, true, 99999) == false; + // the first answer places every cell at once + assert cells_due(3, false, 0) == true; + // after that, one batch a period + assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false; + assert cells_due(1, true, CELL_BATCH_SECONDS) == true; +} + +invariant one_row_per_kind { + assert HIVE_KINDS == 10; + assert FX_OF_KIND[5] == FX_ACCEPT; + assert FLASH_MS < DEPART_MS * 2; + assert CELL_BATCH_SECONDS == 300; +} diff --git a/apps/website/public/t27/files/specs/queen/key_failover.t27 b/apps/website/public/t27/files/specs/queen/key_failover.t27 new file mode 100644 index 0000000000..3ce96bef2b --- /dev/null +++ b/apps/website/public/t27/files/specs/queen/key_failover.t27 @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993). +; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them +; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts +; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48 +; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54. +; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider, +; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no +; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and +; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal +; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not. +; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM +; router's cooldown-then-fallback: +; 1. classify the refusal: transient, quota, auth or a bad request (error_kind); +; 2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes +; it for the long break -- 1302 and 1305 never close a key and never retire work; +; 3. the next attempt goes to the next free key of the pool, never the key that just failed +; (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS +; distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next +; pool instead of burning the rest (pool_overloaded); +; 4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708), +; its claim is released (trios#1886), and the end is not charged to the issue (charges_issue). +; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most +; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides. +; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenKeyFailover; + +; --- 1. What the provider said ----------------------------------------------------------------------- + +/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may +/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused. +/// KE_REQUEST: the request is wrong; every key would answer the same. +pub const KE_OK : u8 = 0; +pub const KE_TRANSIENT : u8 = 1; +pub const KE_QUOTA : u8 = 2; +pub const KE_AUTH : u8 = 3; +pub const KE_REQUEST : u8 = 4; + +/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own. +pub const ZAI_RATE_LIMIT : u32 = 1302; +pub const ZAI_OVERLOAD : u32 = 1305; + +/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321 +/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301). +pub fn is_quota_code(code: u32) -> bool { + if (code == 1113) { return true; } + if (code == 1312) { return false; } + if (code < 1308) { return false; } + return code <= 1321; +} + +/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is +/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai +/// sends a spent quota and a passing overload with the same 429. +pub fn error_kind(status: u32, zai_code: u32) -> u8 { + if (is_quota_code(zai_code)) { return KE_QUOTA; } + if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; } + if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; } + if (status == 0) { return KE_TRANSIENT; } + if (status == 402) { return KE_QUOTA; } + if (status == 401) { return KE_AUTH; } + if (status == 403) { return KE_AUTH; } + if (status == 408) { return KE_TRANSIENT; } + if (status == 429) { return KE_TRANSIENT; } + if (status >= 600) { return KE_REQUEST; } + if (status >= 500) { return KE_TRANSIENT; } + if (status >= 400) { return KE_REQUEST; } + return KE_OK; +} + +; --- 2. How long a refused key rests ------------------------------------------------------------------ + +/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than +/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short +/// enough that 34 keys are never all resting through a one-minute burst like 14:08. +pub const COOL_BASE_SECONDS : u32 = 15; +pub const COOL_CAP_SECONDS : u32 = 240; +/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes). +pub const KEY_BREAK_SECONDS : u32 = 1800; + +/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled +/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it. +pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 { + if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; } + if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; } + if (kind != KE_TRANSIENT) { return 0; } + var wait : u32 = COOL_BASE_SECONDS; + var n : u32 = 0; + while (n < strikes) { + if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + return wait; +} + +/// Whether the refusal closes the key for the long break. A transient refusal never does. +pub fn key_closes(kind: u8) -> bool { + if (kind == KE_QUOTA) { return true; } + return kind == KE_AUTH; +} + +; --- 3. The next key, the next pool ------------------------------------------------------------------ + +pub const POOL_MAX : u32 = 32; +pub const NO_KEY : u32 = 255; + +pub fn bit(i: u32) -> u32 { + if (i >= POOL_MAX) { return 0; } + return 1 << i; +} + +pub fn is_set(mask: u32, i: u32) -> bool { + return (mask & bit(i)) != 0; +} + +/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or +/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of +/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than +/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule +/// picks a key inside a pool and a pool inside the pool list. +pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 { + if (size > POOL_MAX) { return NO_KEY; } + var start : u32 = 0; + var count : u32 = size; + if (last < size) { + start = last + 1; + count = size - 1; + } + var k : u32 = 0; + while (k < count) { + var i : u32 = (start + k) % size; + if (is_set(tried | blocked, i) == false) { return i; } + k = k + 1; + } + return NO_KEY; +} + +/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the +/// pool's model overloaded. A pool smaller than that is overloaded when every key said it. +pub const POOL_OVERLOAD_KEYS : u32 = 3; +pub const OVERLOAD_WINDOW_SECONDS : u32 = 60; + +pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool { + if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; } + if (pool_size == 0) { return false; } + return distinct_keys_1305 >= pool_size; +} + +; --- 4. The step after a refusal --------------------------------------------------------------------- + +/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next +/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong. +pub const FA_DONE : u8 = 0; +pub const FA_NEXT_KEY : u8 = 1; +pub const FA_NEXT_POOL : u8 = 2; +pub const FA_GIVE_UP : u8 = 3; +pub const FA_FAIL : u8 = 4; + +/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an +/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys. +pub const MAX_ATTEMPTS_PER_TURN : u32 = 8; + +/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool` +/// are next_free's answers for the current pool and for the pool list (NO_KEY when none). +pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 { + if (kind == KE_OK) { return FA_DONE; } + if (kind >= KE_REQUEST) { return FA_FAIL; } + if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; } + var pool_out : bool = false; + if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); } + if (pool_out == false) { + if (next_key != NO_KEY) { return FA_NEXT_KEY; } + } + if (next_pool != NO_KEY) { return FA_NEXT_POOL; } + return FA_GIVE_UP; +} + +/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded, +/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request +/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges +/// every DX_PROVIDER_ERROR. +pub fn charges_issue(kind: u8) -> bool { + return kind == KE_REQUEST; +} + +; --- 5. Tests ---------------------------------------------------------------------------------------- + +test the_quota_list_is_the_closed_list_of_the_runtime { + assert is_quota_code(1113) == true; + assert is_quota_code(1308) == true; + assert is_quota_code(1311) == true; + assert is_quota_code(1312) == false; + assert is_quota_code(1313) == true; + assert is_quota_code(1321) == true; + assert is_quota_code(1322) == false; + assert is_quota_code(1307) == false; + assert is_quota_code(1112) == false; + assert is_quota_code(1114) == false; + assert is_quota_code(ZAI_RATE_LIMIT) == false; + assert is_quota_code(ZAI_OVERLOAD) == false; + assert is_quota_code(0) == false; +} + +test the_code_wins_over_the_status { + assert error_kind(429, 1305) == KE_TRANSIENT; + assert error_kind(429, 1302) == KE_TRANSIENT; + assert error_kind(429, 1113) == KE_QUOTA; + assert error_kind(429, 1316) == KE_QUOTA; + assert error_kind(200, 1308) == KE_QUOTA; + assert error_kind(400, 1305) == KE_TRANSIENT; + assert error_kind(400, 1302) == KE_TRANSIENT; + assert error_kind(429, 1312) == KE_TRANSIENT; +} + +test a_status_without_a_code_falls_in_its_kind { + assert error_kind(0, 0) == KE_TRANSIENT; + assert error_kind(200, 0) == KE_OK; + assert error_kind(399, 0) == KE_OK; + assert error_kind(400, 0) == KE_REQUEST; + assert error_kind(401, 0) == KE_AUTH; + assert error_kind(402, 0) == KE_QUOTA; + assert error_kind(403, 0) == KE_AUTH; + assert error_kind(404, 0) == KE_REQUEST; + assert error_kind(407, 0) == KE_REQUEST; + assert error_kind(408, 0) == KE_TRANSIENT; + assert error_kind(409, 0) == KE_REQUEST; + assert error_kind(428, 0) == KE_REQUEST; + assert error_kind(429, 0) == KE_TRANSIENT; + assert error_kind(430, 0) == KE_REQUEST; + assert error_kind(499, 0) == KE_REQUEST; + assert error_kind(500, 0) == KE_TRANSIENT; + assert error_kind(503, 0) == KE_TRANSIENT; + assert error_kind(529, 0) == KE_TRANSIENT; + assert error_kind(599, 0) == KE_TRANSIENT; + assert error_kind(600, 0) == KE_REQUEST; +} + +test a_transient_refusal_rests_seconds_and_a_spent_key_closes { + assert cooldown_seconds(KE_TRANSIENT, 0) == 15; + assert cooldown_seconds(KE_TRANSIENT, 1) == 30; + assert cooldown_seconds(KE_TRANSIENT, 2) == 60; + assert cooldown_seconds(KE_TRANSIENT, 3) == 120; + assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_OK, 3) == 0; + assert cooldown_seconds(KE_REQUEST, 3) == 0; + assert key_closes(KE_QUOTA) == true; + assert key_closes(KE_AUTH) == true; + assert key_closes(KE_TRANSIENT) == false; + assert key_closes(KE_OK) == false; + assert key_closes(KE_REQUEST) == false; +} + +test bits_name_keys { + assert bit(0) == 1; + assert bit(3) == 8; + assert bit(31) == 2147483648; + assert bit(32) == 0; + assert is_set(8, 3) == true; + assert is_set(8, 2) == false; + assert is_set(4294967295, 31) == true; + assert is_set(4294967295, 32) == false; +} + +test the_next_key_is_never_the_one_that_failed { + // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0 + assert next_free(bit(9), 0, 10, 9) == 0; + // even when the failed key is not marked tried, it is skipped + assert next_free(0, 0, 10, 9) == 0; + assert next_free(0, 0, 10, 4) == 5; + // a pool of one key has no other key + assert next_free(0, 0, 1, 0) == NO_KEY; + // tried and resting keys are both skipped + assert next_free(bit(5), bit(6), 10, 4) == 7; + assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY; + // a fresh turn starts at key 0 + assert next_free(0, 0, 24, NO_KEY) == 0; + assert next_free(bit(0), 0, 24, NO_KEY) == 1; + // every key but the failed one is taken + assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY; + // an empty pool has nothing + assert next_free(0, 0, 0, NO_KEY) == NO_KEY; + // `last` just past the end is out of range: the scan starts at 0 + assert next_free(0, 0, 10, 10) == 0; + // a full mask of POOL_MAX keys wraps from the top index + assert next_free(0, 0, 32, 31) == 0; + assert next_free(0, 0, 32, NO_KEY) == 0; + assert next_free(0, 4294967295 - bit(31), 32, 30) == 31; + // a list longer than POOL_MAX does not fit a mask and is refused + assert next_free(0, 0, 33, NO_KEY) == NO_KEY; + assert next_free(0, 0, 40, 31) == NO_KEY; +} + +test an_overload_seen_on_three_keys_is_the_pools_model { + assert pool_overloaded(0, 24) == false; + assert pool_overloaded(2, 24) == false; + assert pool_overloaded(3, 24) == true; + assert pool_overloaded(16, 24) == true; + assert pool_overloaded(2, 2) == true; + assert pool_overloaded(1, 2) == false; + assert pool_overloaded(1, 1) == true; + assert pool_overloaded(0, 0) == false; +} + +/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that +/// answered 1305 in the window, pool size, next pool. +test conformance_rows { + // r01 answered: go on + assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE; + // r02 a wrong request fails, whatever keys are left + assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL; + // r03 an unknown kind is read as a wrong request + assert step(9, 1, 5, 0, 24, 1) == FA_FAIL; + // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key + assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY; + // r05 a rate limit (1302) on a fresh pool: next key + assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY; + // r06 the third distinct key said 1305: the model is overloaded, next pool + assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL; + // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest + assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP; + // r08 a spent quota on one key does not mean an overloaded pool: next key + assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY; + // r09 a refused key: next key + assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY; + // r10 no key left in the pool: next pool + assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL; + // r11 no key and no pool left: give up + assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP; + // r12 the attempt bound stops a turn with keys still left + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP; + // r13 one attempt below the bound still moves on + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY; + // r14 a two-key pool where both said 1305 is overloaded + assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL; + // r15 a 503 with no 1305 seen keeps to the pool + assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY; + // r16 the bound beats a wrong request? no: a wrong request fails first + assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL; + // r17 an answer after many attempts is still an answer + assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE; +} + +test an_overloaded_provider_never_retires_the_issue { + assert charges_issue(KE_TRANSIENT) == false; + assert charges_issue(KE_QUOTA) == false; + assert charges_issue(KE_AUTH) == false; + assert charges_issue(KE_REQUEST) == true; + assert charges_issue(KE_OK) == false; + assert charges_issue(9) == false; +} + +; --- 6. Invariants ----------------------------------------------------------------------------------- + +invariant the_kinds_and_steps_are_distinct { + assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4; + assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4; +} + +/// A transient rest is always shorter than the long break, so a 1305 can never close a key. +invariant a_rest_is_shorter_than_a_break { + assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16; + assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800; +} + +/// The bound leaves room to see an overload in one pool and still try the next: the overload is +/// detected before the bound stops the turn. +invariant the_bound_leaves_room_for_a_second_pool { + assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2; + assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0; +} + +/// NO_KEY is no index a mask can hold. +invariant no_key_is_out_of_range { + assert POOL_MAX == 32 and NO_KEY >= POOL_MAX; + assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305; +} diff --git a/apps/website/public/t27/files/specs/queen/review_first.t27 b/apps/website/public/t27/files/specs/queen/review_first.t27 new file mode 100644 index 0000000000..dfce350f1d --- /dev/null +++ b/apps/website/public/t27/files/specs/queen/review_first.t27 @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/review_first.t27 -- no new work from the backlog while finished work waits in review (gHashTag/t27#10555). +; The owner's rule, 2026-10-18: the Queen takes nothing from the backlog while there is a queue in review. +; Measured 2026-10-18 17:09Z on api.t27.ai/queen/status: 69 of 70 workers busy; in the last hour 102 +; dispatched, 56 finished, 11 reviewed; reviewable (the review sweep's own queue) 29, unreviewed 362. +; The swarm starts about nine bees for every verdict, so the queue in review only grows, and each new bee +; holds files (claim.t27) that the bees already in review need when they are sent back. +; The rule: +; 1. the review queue is exactly the review sweep's own filter, the one the status page counts as +; `reviewable`: the dispatch started, it finished, its review_state is NULL or 'wait', it was not +; reaped, and its issue is still open (in_review_queue); +; 2. while that queue is longer than REVIEW_QUEUE_MAX (0: not while anything waits) a candidate from +; the backlog is refused, and the round says why (admission, R_REVIEW_FIRST); +; 3. work the review itself asked for is never backlog: an issue the review sent back is dispatched +; again at any queue length (KIND_REWORK), and so is an issue the owner assigned by hand +; (KIND_ASSIGNED, the supervisor's pendingAssigns); +; 4. the queue empties on its own: every outcome of the review that is not an accept has a clock +; (review_valve.t27), so a closed backlog always reopens without a person. +; The host counts the queue and names each candidate's kind; this file decides. Not here: the wiring into +; trios (queen-tick.ts candidate filter, vendored wasm and t27c gen-ts constants), how keys are shared +; between bees and the reviewer, the review verdicts themselves. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenReviewFirst; + +; --- 1. What a candidate is --------------------------------------------------------------------------- + +/// KIND_BACKLOG: an open issue with no dispatch the review is working on. KIND_REWORK: an issue whose +/// last dispatch the review sent back (review_state 'sendBack'). KIND_ASSIGNED: an issue the owner +/// assigned by hand. The codes are mirrored by number: a new kind takes the next code, none moves. +pub const KIND_BACKLOG : u8 = 0; +pub const KIND_REWORK : u8 = 1; +pub const KIND_ASSIGNED : u8 = 2; + +/// The owner's hand wins over the review's send-back, and both win over the backlog. +pub fn kind_of(assigned: bool, sent_back: bool) -> u8 { + if (assigned) { return KIND_ASSIGNED; } + if (sent_back) { return KIND_REWORK; } + return KIND_BACKLOG; +} + +; --- 2. What the review queue is ---------------------------------------------------------------------- + +/// A dispatch row's review_state as the host reads it: RS_NONE is NULL (no verdict yet), RS_WAIT is +/// 'wait' (fewer verdicts than criteria, dispatch.t27 REVIEW_RULE), RS_OTHER is any written verdict. +pub const RS_NONE : u8 = 0; +pub const RS_WAIT : u8 = 1; +pub const RS_OTHER : u8 = 2; + +/// Whether one dispatch row is in the review queue: the sweep's own filter (trios queen-tick.ts +/// reviewFinishedDispatches, counted as `reviewable` by queen-public-status.ts). A row whose issue was +/// closed is never reviewed, so it never holds the backlog shut. +pub fn in_review_queue(started: bool, finished: bool, state: u8, reaped: bool, issue_open: bool) -> bool { + if (started == false) { return false; } + if (finished == false) { return false; } + if (reaped) { return false; } + if (issue_open == false) { return false; } + if (state == RS_NONE) { return true; } + return state == RS_WAIT; +} + +; --- 3. Admission ------------------------------------------------------------------------------------- + +/// How many dispatches may wait in review while the backlog stays open. Zero, the owner's words: not +/// while anything is in review. +pub const REVIEW_QUEUE_MAX : u32 = 0; + +/// R_ADMIT: the candidate may be chosen. R_REVIEW_FIRST: refused, the backlog is closed while the review +/// queue is longer than REVIEW_QUEUE_MAX. +pub const R_ADMIT : u8 = 0; +pub const R_REVIEW_FIRST : u8 = 1; + +/// The refusal as the round reports it, after the queue length: "29 dispatches wait in review; ...". +pub const REFUSAL_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty"; + +pub fn backlog_open(review_queue: u32) -> bool { + return review_queue <= REVIEW_QUEUE_MAX; +} + +pub fn admission(kind: u8, review_queue: u32) -> u8 { + if (kind == KIND_REWORK) { return R_ADMIT; } + if (kind == KIND_ASSIGNED) { return R_ADMIT; } + if (backlog_open(review_queue)) { return R_ADMIT; } + return R_REVIEW_FIRST; +} + +pub fn admits(kind: u8, review_queue: u32) -> bool { + return admission(kind, review_queue) == R_ADMIT; +} + +/// Whether a round that admitted no candidate owes the review-first sentence: some candidate was +/// refused by this rule and none was admitted. +pub fn round_refused(admitted: u32, refused: u32) -> bool { + if (admitted > 0) { return false; } + return refused > 0; +} + +; --- 4. Tests ----------------------------------------------------------------------------------------- + +test the_owner_hand_wins_then_the_review_then_the_backlog { + assert kind_of(false, false) == 0; + assert kind_of(false, true) == 1; + assert kind_of(true, false) == 2; + assert kind_of(true, true) == 2; +} + +test the_queue_is_the_sweeps_own_filter { + assert in_review_queue(true, true, RS_NONE, false, true) == true; + assert in_review_queue(true, true, RS_WAIT, false, true) == true; + assert in_review_queue(true, true, RS_OTHER, false, true) == false; + assert in_review_queue(true, false, RS_NONE, false, true) == false; + assert in_review_queue(false, true, RS_NONE, false, true) == false; + assert in_review_queue(true, true, RS_NONE, true, true) == false; + assert in_review_queue(true, true, RS_NONE, false, false) == false; + assert in_review_queue(true, true, RS_WAIT, true, true) == false; + assert in_review_queue(true, true, 7, false, true) == false; +} + +test the_backlog_is_closed_while_anything_waits_in_review { + assert admission(KIND_BACKLOG, 0) == 0; + assert admission(KIND_BACKLOG, 1) == 1; + assert admission(KIND_BACKLOG, 29) == 1; + assert admission(KIND_BACKLOG, 4294967295) == 1; + assert backlog_open(0) == true; + assert backlog_open(1) == false; + assert admits(KIND_BACKLOG, 0) == true; + assert admits(KIND_BACKLOG, 1) == false; +} + +test work_the_review_asked_for_is_never_backlog { + assert admission(KIND_REWORK, 0) == 0; + assert admission(KIND_REWORK, 1) == 0; + assert admission(KIND_REWORK, 29) == 0; + assert admission(KIND_REWORK, 4294967295) == 0; + assert admits(KIND_REWORK, 362) == true; +} + +test the_owners_assignment_is_never_held { + assert admission(KIND_ASSIGNED, 0) == 0; + assert admission(KIND_ASSIGNED, 29) == 0; + assert admits(KIND_ASSIGNED, 4294967295) == true; +} + +test an_unknown_kind_is_backlog_and_is_held { + assert admission(3, 0) == 0; + assert admission(3, 1) == 1; + assert admission(255, 29) == 1; +} + +test a_round_reports_the_rule_only_when_it_stopped_everything { + assert round_refused(0, 0) == false; + assert round_refused(0, 5) == true; + assert round_refused(1, 5) == false; + assert round_refused(3, 0) == false; +} + +test the_measured_round_of_2026_10_18 { + assert admits(kind_of(false, false), 29) == false; + assert admits(kind_of(false, true), 29) == true; + assert admits(kind_of(true, false), 29) == true; + assert admits(kind_of(false, false), 0) == true; +} + +; --- 5. Invariants ------------------------------------------------------------------------------------ + +invariant the_codes_never_move { + assert KIND_BACKLOG == 0 and KIND_REWORK == 1 and KIND_ASSIGNED == 2; + assert RS_NONE == 0 and RS_WAIT == 1 and RS_OTHER == 2; + assert R_ADMIT == 0 and R_REVIEW_FIRST == 1; +} + +/// The owner's bound: nothing may wait in review while the backlog is open. +invariant the_bound_is_the_owners { + assert REVIEW_QUEUE_MAX == 0; +} diff --git a/apps/website/public/t27/files/specs/queen/reviewer.t27 b/apps/website/public/t27/files/specs/queen/reviewer.t27 new file mode 100644 index 0000000000..dcc617112b --- /dev/null +++ b/apps/website/public/t27/files/specs/queen/reviewer.t27 @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time +; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the +; live Queen (gHashTag/BrowserOS trios/agent-server): +; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91); +; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after +; another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to +; 2 minutes -- in practice about one review a round; +; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min). +; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each +; on its own lane, one row never twice at once, and every waiting row visited in turn. The round +; then reviews nothing: two sweeps over one row would race on its verdict. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenReviewer; + +/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy +/// costs one query. +pub const REVIEWER_EVERY_SECONDS : u32 = 10; + +/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the +/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds). +/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays +/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour. +pub const REVIEWER_CONCURRENCY : u32 = 4; + +/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and +/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`). +pub const REVIEWER_CEILING : u32 = 16; + +/// Seconds in an hour; finish rates are measured per hour. +pub const SECONDS_PER_HOUR : u32 = 3600; + +/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer +/// shares a 45 s budget with the round, because it no longer holds the round up. +pub const REVIEW_ROW_SECONDS : u32 = 300; + +/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58 +/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS +/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below +/// the finish rate even if every review used its whole budget, and the backlog only grows. +/// How many rows the reviewer may take at once, from what the host measures: +/// - `keys` model keys, each with `lanes_per_key` lanes free for review; +/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review); +/// - never above REVIEWER_CEILING, never above the rows `waiting`; +/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains. +pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 { + if (waiting == 0) { return 0; } + var k : u32 = keys; + if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; } + var l : u32 = lanes_per_key; + if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; } + var bound : u32 = k * l; + if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; } + var by_memory : u32 = 1; + if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; } + if (by_memory < bound) { bound = by_memory; } + if (waiting < bound) { bound = waiting; } + if (bound < 1) { return 1; } + return bound; +} + +/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every +/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600). +/// Split by the hour so no u32 product overflows. +pub fn slots_needed(finished_per_hour: u32) -> u32 { + let whole : u32 = finished_per_hour / SECONDS_PER_HOUR; + let part : u32 = finished_per_hour % SECONDS_PER_HOUR; + let part_seconds : u32 = part * REVIEW_ROW_SECONDS; + var extra : u32 = part_seconds / SECONDS_PER_HOUR; + if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; } + return whole * REVIEW_ROW_SECONDS + extra; +} + +/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows. +pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool { + return slots >= slots_needed(finished_per_hour); +} + +/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but +/// no more than the rows waiting. +pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 { + if (in_flight >= bound) { return 0; } + let room : u32 = bound - in_flight; + if (waiting < room) { return waiting; } + return room; +} + +/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed +/// REVIEWER_CONCURRENCY, unchanged for its existing callers. +pub fn review_slots(in_flight: u32, waiting: u32) -> u32 { + return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting); +} + +/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never +/// visited before any it has. Without it, a row that is re-read and waits again (no review spent) +/// keeps its place at the head and the reviewer circles the same few rows. +pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool { + if (never_a and never_b == false) { return true; } + if (never_b and never_a == false) { return false; } + return seconds_since_a > seconds_since_b; +} + +/// While the reviewer runs, the round reviews nothing. +pub fn round_reviews(reviewer_running: bool) -> bool { + return reviewer_running == false; +} + +/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen, +/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could +/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row +/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned +/// about 430 visits of the reviewer's slots. +/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never +/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that +/// has started to read is visited at the normal pace again. +pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30; +pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900; + +pub fn revisit_after_seconds(waits: u32) -> u32 { + if (waits == 0) { return 0; } + var wait : u32 = WAIT_REVISIT_BASE_SECONDS; + var n : u32 = 1; + while (n < waits) { + if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + return wait; +} + +/// Whether a row may be taken now: one that never waited always may, and one that waited only after +/// its revisit time has passed since the last visit. +pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool { + return seconds_since_visit >= revisit_after_seconds(waits); +} + +/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the +/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait` +/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only +/// when the bee committed something, so a bee that committed nothing leaves no branch here, the +/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed. +/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free +/// again after the retry floor. +/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle +/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit, +/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty +/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits, +/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner` +/// false) and a row whose bundle is stored are never decided here: their diff failed for some +/// other reason, and that still decides nothing. +pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10; + +pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool { + if (on_runner == false) { return false; } + if (bundle_stored) { return false; } + return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES; +} + +test a_pass_fills_the_free_slots_with_waiting_rows { + assert review_slots(0, 0) == 0; + assert review_slots(0, 2) == 2; + assert review_slots(0, 100) == REVIEWER_CONCURRENCY; + assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1; + assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1; + assert review_slots(REVIEWER_CONCURRENCY, 100) == 0; + assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0; +} + +test the_bound_follows_lanes_memory_and_backlog { + // nothing waits: no slot, whatever the host has + assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0; + // lanes bound: 2 keys x 3 lanes + assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6; + // memory bound: 1000 MB at 300 MB a review + assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3; + // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free + assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16; + assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING; + // never above the backlog + assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2; + // at least one while anything waits, even with no lane or memory measured + assert reviewer_concurrency(0, 0, 0, 0, 1) == 1; + assert reviewer_concurrency(10, 10, 100, 300, 111) == 1; + assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1; + assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1; +} + +test the_measured_load_needs_five_and_the_bound_admits_them { + // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5 + assert slots_needed(58) == 5; + assert slots_needed(0) == 0; + assert slots_needed(12) == 1; + assert slots_needed(13) == 2; + assert slots_needed(3600) == REVIEW_ROW_SECONDS; + assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1; + assert slots_needed(4294967295) == 357913942; + // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true; + assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16; + assert keeps_up(4, 48) == true; + assert keeps_up(4, 49) == false; +} + +test a_pass_fills_the_free_slots_against_the_computed_bound { + let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111); + assert review_slots_within(bound, 0, 111) == 16; + assert review_slots_within(bound, 11, 111) == 5; + assert review_slots_within(bound, 15, 3) == 1; + assert review_slots_within(bound, 16, 111) == 0; + assert review_slots_within(bound, 17, 111) == 0; + assert review_slots_within(bound, 0, 3) == 3; + assert review_slots_within(0, 0, 111) == 0; +} + +test every_row_is_visited_in_turn { + // never visited goes first + assert visit_first(true, 0, false, 9999) == true; + assert visit_first(false, 9999, true, 0) == false; + // otherwise the one visited longest ago + assert visit_first(false, 300, false, 20) == true; + assert visit_first(false, 20, false, 300) == false; + assert visit_first(false, 20, false, 20) == false; + assert visit_first(true, 0, true, 0) == false; +} + +test the_round_stands_aside_for_the_reviewer { + assert round_reviews(true) == false; + assert round_reviews(false) == true; +} + +test a_row_that_keeps_waiting_is_visited_less_and_less { + assert revisit_after_seconds(0) == 0; + assert revisit_after_seconds(1) == 30; + assert revisit_after_seconds(2) == 60; + assert revisit_after_seconds(3) == 120; + assert revisit_after_seconds(5) == 480; + assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS; + // a row that never waited is always due; one that waited is due only once its time has passed + assert due_again(0, 0) == true; + assert due_again(1, 29) == false; + assert due_again(1, 30) == true; + assert due_again(6, 899) == false; + assert due_again(6, 900) == true; +} + +test no_bundle_is_empty_for_a_runner_row_past_the_grace { + // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true; + // the six hours the frozen-wait valve would otherwise have held it + assert no_bundle_is_empty(true, false, 360) == true; + assert no_bundle_is_empty(true, false, 4294967295) == true; +} + +test no_bundle_is_empty_waits_inside_the_grace { + // just finished: the runner may not have stored its bundle yet + assert no_bundle_is_empty(true, false, 0) == false; + // a bundle 40 s late is still awaited, and judged on its branch when it arrives + assert no_bundle_is_empty(true, false, 1) == false; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false; +} + +test no_bundle_is_empty_leaves_a_container_row_alone { + // a bee that ran here has no bundle by design; its diff failing decides nothing + assert no_bundle_is_empty(false, false, 0) == false; + assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(false, false, 4294967295) == false; + assert no_bundle_is_empty(false, true, 4294967295) == false; +} + +test no_bundle_is_empty_leaves_a_stored_bundle_alone { + // the bundle is there, so the diff failed for another reason: still a wait + assert no_bundle_is_empty(true, true, 0) == false; + assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(true, true, 4294967295) == false; +} + +invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour { + // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit + assert NO_BUNDLE_GRACE_MINUTES > 1; + assert no_bundle_is_empty(true, false, 1) == false; + // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit + // that judges it comes within the hour of its finish, not after the six-hour valve + assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR; +} + +invariant the_reviewer_is_quicker_than_the_round { + assert REVIEWER_EVERY_SECONDS < 60; + assert REVIEWER_CONCURRENCY > 1; + assert REVIEW_ROW_SECONDS >= 120; + // a stuck row is still seen several times an hour, so one that starts to read is not lost for long + assert WAIT_REVISIT_CAP_SECONDS <= 3600; + assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS; +} + +invariant the_reviewer_can_outrun_the_measured_finish_rate { + // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets + // at least the slots that load needs, and the ceiling is not what stops it + assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58); + assert REVIEWER_CEILING >= slots_needed(58); + assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY; + // the fixed bound alone could not, which is why it is only the fallback + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours + assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0; + // FR-001: 0 only when nothing waits + assert reviewer_concurrency(0, 0, 0, 0, 0) == 0; + assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1; +} diff --git a/apps/website/public/widgets/gallery.t27 b/apps/website/public/widgets/gallery.t27 index 17117ac2a9..fb07268ccf 100644 --- a/apps/website/public/widgets/gallery.t27 +++ b/apps/website/public/widgets/gallery.t27 @@ -78,15 +78,15 @@ pub const TOOL_NOSCRIPT : str = "This widget runs in your browser and needs Java ; Parallel arrays, WIDGET_COUNT long. WIDGET_TITLES is empty wherever the page carries an ; og:title, which is every kind but tab: the page is the one place the words live. A tool's ; WIDGET_HOOKS entry is empty too: its hook is the page's t27:hook, written from its own spec. -pub const WIDGET_COUNT : u16 = 279; -pub const WIDGET_IDS : [279]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "bit-diff", "build-receipt", "cost-curves", "gf16-calc", "idcode", "lut-explain", "pin-map", "placement-map", "resource-bars", "slack-waterfall", "test-waves", "synth-smells", "bit-autopsy", "fasm-skyline", "vcd-wrapped", "fits-on", "stat-card", "lut-depth", "fsm-sketch", "lut-treemap", "t27-cell", "t27-vs-nvfp4", "uart-bucket", "tri-fpga-specs", "tri-fpga-tools", "tri-fpga-wire", "tri-fpga-batch", "tri-fpga-status", "tri-fpga-tmpcheck", "tri-fpga-keycheck", "tri-fpga-selftest", "tri-x7-refs", "tri-x7-audit", "t27c-clock-domain", "t27c-clocks", "t27c-wukong-v1", "t27c-cts", "t27c-mmcm", "t27c-mmcm-mutate", "t27c-timing", "t27c-reset-sync", "t27c-reset-sync-gen", "t27c-mtbf", "t27c-clock-domain-gen", "t27c-fifo", "t27c-fifo-mutate", "t27c-fifo-flags", "t27c-emitter-xdc", "t27c-fpga-rxcap", "t27-tri-competitors", "t27-tri-discard", "tri-fpga-txhold", "tri-fpga-rxcap", "tri-fpga-rxer", "tri-fpga-steps", "tri-fpga-ioclients", "tri-fpga-claim", "tri-fpga-release", "tri-fpga-next", "t27c-memory", "t27c-spi-tb", "t27c-check-uart", "t27c-check-spi", "t27c-check-apb", "t27c-check-axi4", "t27c-check-bridge", "t27c-check-memory", "t27c-verilog-uart", "t27c-verilog-spi", "t27c-verilog-apb", "t27c-verilog-axi4", "t27c-verilog-bridge", "t27c-verilog-top", "t27c-verilog-uart-tb", "t27c-verilog-apb-tb", "t27c-verilog-axi-tb", "t27c-verilog-mem-tb", "t27c-verilog-integration-tb", "t27c-verilog-top-tb", "t27c-verilog-spi-tb", "t27c-hir-uart", "t27c-hir-apb", "t27c-hir-axi4", "t27c-eth-crc", "t27c-rgmii", "jtag-verdict", "line-bounce", "eye-isi", "ecc-secded", "carry-chain", "prefix-adder", "csa-tree", "shift-add", "booth-radix4", "dsp-fit", "q-format", "round-sat", "sqnr", "cordic", "cordic-vector", "sine-table", "alias-fold", "nco", "dft-bin", "fir-response", "fir-design", "fir-cost", "decimator", "cic-growth", "polyphase", "fft-butterfly", "bit-reverse", "fft-scaling", "barker-corr", "fir-fit", "fir-on-board", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "test-touch", "t27c-stages", "t27c-e8m0", "play-e8m0", "t27c-ocp-mx", "play-gfternary", "t27c-gfternary", "play-golden-sieve", "t27c-golden-sieve", "play-tnf17", "t27c-tnf17", "t27c-activation-quantizer", "t27c-bitnet-majority", "t27c-bitnet-neuron-nchunk", "t27c-comb-ternary-dot", "t27c-stream-ternary-mac", "t27c-bitnet-mlp", "t27c-gft-smul", "t27c-gft-sadd", "t27c-gft-signed-mac", "t27c-gft-relu", "t27c-gft-exp2", "t27c-gft-argmax4", "t27c-gft-nll", "t27c-gft-sgd-step", "t27c-gft-xornet", "basics-what-a-spec-is", "basics-the-module-line", "basics-comments-and-prose", "basics-constants", "basics-whole-numbers", "basics-true-false-and-text", "basics-arrays", "basics-trits", "basics-expressions", "basics-a-test-block", "basics-many-tests", "basics-functions", "basics-local-names", "basics-if-and-else", "basics-loops", "basics-pub-or-private", "basics-structs", "basics-enums", "basics-use-other-modules", "basics-invariants", "basics-gen-ts", "basics-seven-backends", "basics-stages-of-t27c", "basics-tri-commands", "basics-reading-errors", "basics-a-small-program", "basics-where-next", "golden-split", "golden-phi-ratio", "golden-lucas", "golden-split-optimality", "golden-radix-economy", "golden-phi-in-gf16", "golden-gf4", "golden-gf6", "golden-gf8", "golden-gf10", "golden-gf12", "golden-gf14", "golden-gf16", "golden-dot-two", "golden-gf24", "golden-gf32", "golden-gf48", "golden-gf64", "golden-gf96", "golden-gf128", "golden-gf256", "golden-gf512", "golden-gf1024", "golden-gft8", "golden-gft16", "golden-gft32", "golden-decode", "t27-tri-vectors", "t27-tri-census", "t27-tri-seals", "t27-tri-prose", "t27-tri-lean", "tri-selftest", "play-terminal-gates", "tc-why-three", "tc-balanced-count", "tc-flip-and-cut", "tc-kleene-gates", "tc-consensus-any", "tc-sum-and-carry", "tc-half-adder", "tc-full-adder", "tc-ripple-carry", "tc-times-a-trit", "tc-long-multiply", "tc-ternary-mac", "tc-two-bits", "tc-five-in-a-byte", "tc-trit-word", "tc-instruction-word", "tc-opcode-map", "tc-immediate", "tc-ternary-alu", "tc-three-way-jump", "tc-run-a-program", "tc-compare", "tc-thirds", "tc-sort-three", "tc-one-neuron", "tc-detector", "tc-tiny-layer", "nn-relu", "nn-sigmoid", "nn-tanh", "nn-gelu", "nn-silu", "nn-elu", "nn-softmax", "nn-embedding", "nn-dropout", "nn-conv", "nn-maxpool", "nn-batchnorm", "nn-mse", "nn-bce", "nn-huber", "nn-sgd", "nn-rmsprop", "nn-adam", "nn-backprop", "nn-xor", "nn-lamb", "nn-rnn", "nn-lstm", "nn-attention", "nn-positions", "nn-encoder", "nn-ppo", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts", "race-the-bee", "gatle", "tri-blog-list", "tri-game-selftest"]; -pub const WIDGET_KINDS : [279]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "player", "player", "player", "cast", "cast", "tool", "tool", "cast", "player", "cast", "player", "cast", "player", "cast", "player", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tab", "cast", "cast", "cast", "tool", "tool", "cast", "cast"]; -pub const WIDGET_CATEGORIES : [279]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game", "game", "game", "game", "game"]; -pub const WIDGET_PAGES : [279]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "widgets/bit-diff/", "widgets/build-receipt/", "widgets/cost-curves/", "widgets/gf16-calc/", "widgets/idcode/", "widgets/lut-explain/", "widgets/pin-map/", "widgets/placement-map/", "widgets/resource-bars/", "widgets/slack-waterfall/", "widgets/test-waves/", "widgets/synth-smells/", "widgets/bit-autopsy/", "widgets/fasm-skyline/", "widgets/vcd-wrapped/", "widgets/fits-on/", "widgets/stat-card/", "widgets/lut-depth/", "widgets/fsm-sketch/", "widgets/lut-treemap/", "widgets/t27-cell/", "widgets/t27-vs-nvfp4/", "widgets/uart-bucket/", "term/tri-fpga-specs/", "term/tri-fpga-tools/", "term/tri-fpga-wire/", "term/tri-fpga-batch/", "term/tri-fpga-status/", "term/tri-fpga-tmpcheck/", "term/tri-fpga-keycheck/", "term/tri-fpga-selftest/", "term/tri-x7-refs/", "term/tri-x7-audit/", "term/t27c-clock-domain/", "term/t27c-clocks/", "term/t27c-wukong-v1/", "term/t27c-cts/", "term/t27c-mmcm/", "term/t27c-mmcm-mutate/", "term/t27c-timing/", "term/t27c-reset-sync/", "term/t27c-reset-sync-gen/", "term/t27c-mtbf/", "term/t27c-clock-domain-gen/", "term/t27c-fifo/", "term/t27c-fifo-mutate/", "term/t27c-fifo-flags/", "term/t27c-emitter-xdc/", "term/t27c-fpga-rxcap/", "term/t27-tri-competitors/", "term/t27-tri-discard/", "term/tri-fpga-txhold/", "term/tri-fpga-rxcap/", "term/tri-fpga-rxer/", "term/tri-fpga-steps/", "term/tri-fpga-ioclients/", "term/tri-fpga-claim/", "term/tri-fpga-release/", "term/tri-fpga-next/", "term/t27c-memory/", "term/t27c-spi-tb/", "term/t27c-check-uart/", "term/t27c-check-spi/", "term/t27c-check-apb/", "term/t27c-check-axi4/", "term/t27c-check-bridge/", "term/t27c-check-memory/", "term/t27c-verilog-uart/", "term/t27c-verilog-spi/", "term/t27c-verilog-apb/", "term/t27c-verilog-axi4/", "term/t27c-verilog-bridge/", "term/t27c-verilog-top/", "term/t27c-verilog-uart-tb/", "term/t27c-verilog-apb-tb/", "term/t27c-verilog-axi-tb/", "term/t27c-verilog-mem-tb/", "term/t27c-verilog-integration-tb/", "term/t27c-verilog-top-tb/", "term/t27c-verilog-spi-tb/", "term/t27c-hir-uart/", "term/t27c-hir-apb/", "term/t27c-hir-axi4/", "term/t27c-eth-crc/", "term/t27c-rgmii/", "widgets/jtag-verdict/", "widgets/line-bounce/", "widgets/eye-isi/", "widgets/ecc-secded/", "widgets/carry-chain/", "widgets/prefix-adder/", "widgets/csa-tree/", "widgets/shift-add/", "widgets/booth-radix4/", "widgets/dsp-fit/", "widgets/q-format/", "widgets/round-sat/", "widgets/sqnr/", "widgets/cordic/", "widgets/cordic-vector/", "widgets/sine-table/", "widgets/alias-fold/", "widgets/nco/", "widgets/dft-bin/", "widgets/fir-response/", "widgets/fir-design/", "widgets/fir-cost/", "widgets/decimator/", "widgets/cic-growth/", "widgets/polyphase/", "widgets/fft-butterfly/", "widgets/bit-reverse/", "widgets/fft-scaling/", "widgets/barker-corr/", "widgets/fir-fit/", "widgets/fir-on-board/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "widgets/test-touch/", "widgets/t27c-stages/", "term/t27c-e8m0/", "play/e8m0/", "term/t27c-ocp-mx/", "play/gfternary/", "term/t27c-gfternary/", "play/golden-sieve/", "term/t27c-golden-sieve/", "play/tnf17/", "term/t27c-tnf17/", "term/t27c-activation-quantizer/", "term/t27c-bitnet-majority/", "term/t27c-bitnet-neuron-nchunk/", "term/t27c-comb-ternary-dot/", "term/t27c-stream-ternary-mac/", "term/t27c-bitnet-mlp/", "term/t27c-gft-smul/", "term/t27c-gft-sadd/", "term/t27c-gft-signed-mac/", "term/t27c-gft-relu/", "term/t27c-gft-exp2/", "term/t27c-gft-argmax4/", "term/t27c-gft-nll/", "term/t27c-gft-sgd-step/", "term/t27c-gft-xornet/", "widgets/basics-what-a-spec-is/", "widgets/basics-the-module-line/", "widgets/basics-comments-and-prose/", "widgets/basics-constants/", "widgets/basics-whole-numbers/", "widgets/basics-true-false-and-text/", "widgets/basics-arrays/", "widgets/basics-trits/", "widgets/basics-expressions/", "widgets/basics-a-test-block/", "widgets/basics-many-tests/", "widgets/basics-functions/", "widgets/basics-local-names/", "widgets/basics-if-and-else/", "widgets/basics-loops/", "widgets/basics-pub-or-private/", "widgets/basics-structs/", "widgets/basics-enums/", "widgets/basics-use-other-modules/", "widgets/basics-invariants/", "widgets/basics-gen-ts/", "widgets/basics-seven-backends/", "widgets/basics-stages-of-t27c/", "widgets/basics-tri-commands/", "widgets/basics-reading-errors/", "widgets/basics-a-small-program/", "widgets/basics-where-next/", "widgets/golden-split/", "widgets/golden-phi-ratio/", "widgets/golden-lucas/", "widgets/golden-split-optimality/", "widgets/golden-radix-economy/", "widgets/golden-phi-in-gf16/", "widgets/golden-gf4/", "widgets/golden-gf6/", "widgets/golden-gf8/", "widgets/golden-gf10/", "widgets/golden-gf12/", "widgets/golden-gf14/", "widgets/golden-gf16/", "widgets/golden-dot-two/", "widgets/golden-gf24/", "widgets/golden-gf32/", "widgets/golden-gf48/", "widgets/golden-gf64/", "widgets/golden-gf96/", "widgets/golden-gf128/", "widgets/golden-gf256/", "widgets/golden-gf512/", "widgets/golden-gf1024/", "widgets/golden-gft8/", "widgets/golden-gft16/", "widgets/golden-gft32/", "widgets/golden-decode/", "term/t27-tri-vectors/", "term/t27-tri-census/", "term/t27-tri-seals/", "term/t27-tri-prose/", "term/t27-tri-lean/", "term/tri-selftest/", "term/play-terminal-gates/", "widgets/tc-why-three/", "widgets/tc-balanced-count/", "widgets/tc-flip-and-cut/", "widgets/tc-kleene-gates/", "widgets/tc-consensus-any/", "widgets/tc-sum-and-carry/", "widgets/tc-half-adder/", "widgets/tc-full-adder/", "widgets/tc-ripple-carry/", "widgets/tc-times-a-trit/", "widgets/tc-long-multiply/", "widgets/tc-ternary-mac/", "widgets/tc-two-bits/", "widgets/tc-five-in-a-byte/", "widgets/tc-trit-word/", "widgets/tc-instruction-word/", "widgets/tc-opcode-map/", "widgets/tc-immediate/", "widgets/tc-ternary-alu/", "widgets/tc-three-way-jump/", "widgets/tc-run-a-program/", "widgets/tc-compare/", "widgets/tc-thirds/", "widgets/tc-sort-three/", "widgets/tc-one-neuron/", "widgets/tc-detector/", "widgets/tc-tiny-layer/", "widgets/nn-relu/", "widgets/nn-sigmoid/", "widgets/nn-tanh/", "widgets/nn-gelu/", "widgets/nn-silu/", "widgets/nn-elu/", "widgets/nn-softmax/", "widgets/nn-embedding/", "widgets/nn-dropout/", "widgets/nn-conv/", "widgets/nn-maxpool/", "widgets/nn-batchnorm/", "widgets/nn-mse/", "widgets/nn-bce/", "widgets/nn-huber/", "widgets/nn-sgd/", "widgets/nn-rmsprop/", "widgets/nn-adam/", "widgets/nn-backprop/", "widgets/nn-xor/", "widgets/nn-lamb/", "widgets/nn-rnn/", "widgets/nn-lstm/", "widgets/nn-attention/", "widgets/nn-positions/", "widgets/nn-encoder/", "widgets/nn-ppo/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/", "widgets/race-the-bee/", "widgets/gatle/", "term/tri-blog-list/", "term/tri-game-selftest/"]; -pub const WIDGET_TITLES : [279]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", "", "", "", "", ""]; -pub const WIDGET_LINES : [279]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", "", "", "", "", ""]; +pub const WIDGET_COUNT : u16 = 306; +pub const WIDGET_IDS : [306]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "bit-diff", "build-receipt", "cost-curves", "gf16-calc", "idcode", "lut-explain", "pin-map", "placement-map", "resource-bars", "slack-waterfall", "test-waves", "synth-smells", "bit-autopsy", "fasm-skyline", "vcd-wrapped", "fits-on", "stat-card", "lut-depth", "fsm-sketch", "lut-treemap", "t27-cell", "t27-vs-nvfp4", "uart-bucket", "tri-fpga-specs", "tri-fpga-tools", "tri-fpga-wire", "tri-fpga-batch", "tri-fpga-status", "tri-fpga-tmpcheck", "tri-fpga-keycheck", "tri-fpga-selftest", "tri-x7-refs", "tri-x7-audit", "t27c-clock-domain", "t27c-clocks", "t27c-wukong-v1", "t27c-cts", "t27c-mmcm", "t27c-mmcm-mutate", "t27c-timing", "t27c-reset-sync", "t27c-reset-sync-gen", "t27c-mtbf", "t27c-clock-domain-gen", "t27c-fifo", "t27c-fifo-mutate", "t27c-fifo-flags", "t27c-emitter-xdc", "t27c-fpga-rxcap", "t27-tri-competitors", "t27-tri-discard", "tri-fpga-txhold", "tri-fpga-rxcap", "tri-fpga-rxer", "tri-fpga-steps", "tri-fpga-ioclients", "tri-fpga-claim", "tri-fpga-release", "tri-fpga-next", "t27c-memory", "t27c-spi-tb", "t27c-check-uart", "t27c-check-spi", "t27c-check-apb", "t27c-check-axi4", "t27c-check-bridge", "t27c-check-memory", "t27c-verilog-uart", "t27c-verilog-spi", "t27c-verilog-apb", "t27c-verilog-axi4", "t27c-verilog-bridge", "t27c-verilog-top", "t27c-verilog-uart-tb", "t27c-verilog-apb-tb", "t27c-verilog-axi-tb", "t27c-verilog-mem-tb", "t27c-verilog-integration-tb", "t27c-verilog-top-tb", "t27c-verilog-spi-tb", "t27c-hir-uart", "t27c-hir-apb", "t27c-hir-axi4", "t27c-eth-crc", "t27c-rgmii", "jtag-verdict", "line-bounce", "eye-isi", "ecc-secded", "carry-chain", "prefix-adder", "csa-tree", "shift-add", "booth-radix4", "dsp-fit", "q-format", "round-sat", "sqnr", "cordic", "cordic-vector", "sine-table", "alias-fold", "nco", "dft-bin", "fir-response", "fir-design", "fir-cost", "decimator", "cic-growth", "polyphase", "fft-butterfly", "bit-reverse", "fft-scaling", "barker-corr", "fir-fit", "fir-on-board", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "test-touch", "t27c-stages", "t27c-e8m0", "play-e8m0", "t27c-ocp-mx", "play-gfternary", "t27c-gfternary", "play-golden-sieve", "t27c-golden-sieve", "play-tnf17", "t27c-tnf17", "t27c-activation-quantizer", "t27c-bitnet-majority", "t27c-bitnet-neuron-nchunk", "t27c-comb-ternary-dot", "t27c-stream-ternary-mac", "t27c-bitnet-mlp", "t27c-gft-smul", "t27c-gft-sadd", "t27c-gft-signed-mac", "t27c-gft-relu", "t27c-gft-exp2", "t27c-gft-argmax4", "t27c-gft-nll", "t27c-gft-sgd-step", "t27c-gft-xornet", "basics-what-a-spec-is", "basics-the-module-line", "basics-comments-and-prose", "basics-constants", "basics-whole-numbers", "basics-true-false-and-text", "basics-arrays", "basics-trits", "basics-expressions", "basics-a-test-block", "basics-many-tests", "basics-functions", "basics-local-names", "basics-if-and-else", "basics-loops", "basics-pub-or-private", "basics-structs", "basics-enums", "basics-use-other-modules", "basics-invariants", "basics-gen-ts", "basics-seven-backends", "basics-stages-of-t27c", "basics-tri-commands", "basics-reading-errors", "basics-a-small-program", "basics-where-next", "golden-split", "golden-phi-ratio", "golden-lucas", "golden-split-optimality", "golden-radix-economy", "golden-phi-in-gf16", "golden-gf4", "golden-gf6", "golden-gf8", "golden-gf10", "golden-gf12", "golden-gf14", "golden-gf16", "golden-dot-two", "golden-gf24", "golden-gf32", "golden-gf48", "golden-gf64", "golden-gf96", "golden-gf128", "golden-gf256", "golden-gf512", "golden-gf1024", "golden-gft8", "golden-gft16", "golden-gft32", "golden-decode", "t27-tri-vectors", "t27-tri-census", "t27-tri-seals", "t27-tri-prose", "t27-tri-lean", "tri-selftest", "play-terminal-gates", "tc-why-three", "tc-balanced-count", "tc-flip-and-cut", "tc-kleene-gates", "tc-consensus-any", "tc-sum-and-carry", "tc-half-adder", "tc-full-adder", "tc-ripple-carry", "tc-times-a-trit", "tc-long-multiply", "tc-ternary-mac", "tc-two-bits", "tc-five-in-a-byte", "tc-trit-word", "tc-instruction-word", "tc-opcode-map", "tc-immediate", "tc-ternary-alu", "tc-three-way-jump", "tc-run-a-program", "tc-compare", "tc-thirds", "tc-sort-three", "tc-one-neuron", "tc-detector", "tc-tiny-layer", "nn-relu", "nn-sigmoid", "nn-tanh", "nn-gelu", "nn-silu", "nn-elu", "nn-softmax", "nn-embedding", "nn-dropout", "nn-conv", "nn-maxpool", "nn-batchnorm", "nn-mse", "nn-bce", "nn-huber", "nn-sgd", "nn-rmsprop", "nn-adam", "nn-backprop", "nn-xor", "nn-lamb", "nn-rnn", "nn-lstm", "nn-attention", "nn-positions", "nn-encoder", "nn-ppo", "hive-shape", "hive-boundary", "hive-task-filter", "hive-aging", "hive-rank", "hive-review-first", "hive-lifecycle", "hive-ship", "hive-drain", "hive-reviewers", "hive-revisit", "hive-gate", "hive-jam", "hive-jam-action", "hive-master-red", "hive-key-error", "hive-key-rest", "hive-next-key", "hive-credit", "hive-price", "hive-burn", "hive-called", "hive-chain", "hive-cooldown", "hive-night", "hive-column", "hive-cell", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts", "race-the-bee", "gatle", "tri-blog-list", "tri-game-selftest"]; +pub const WIDGET_KINDS : [306]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "player", "player", "player", "cast", "cast", "tool", "tool", "cast", "player", "cast", "player", "cast", "player", "cast", "player", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tab", "cast", "cast", "cast", "tool", "tool", "cast", "cast"]; +pub const WIDGET_CATEGORIES : [306]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game"]; +pub const WIDGET_PAGES : [306]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "widgets/bit-diff/", "widgets/build-receipt/", "widgets/cost-curves/", "widgets/gf16-calc/", "widgets/idcode/", "widgets/lut-explain/", "widgets/pin-map/", "widgets/placement-map/", "widgets/resource-bars/", "widgets/slack-waterfall/", "widgets/test-waves/", "widgets/synth-smells/", "widgets/bit-autopsy/", "widgets/fasm-skyline/", "widgets/vcd-wrapped/", "widgets/fits-on/", "widgets/stat-card/", "widgets/lut-depth/", "widgets/fsm-sketch/", "widgets/lut-treemap/", "widgets/t27-cell/", "widgets/t27-vs-nvfp4/", "widgets/uart-bucket/", "term/tri-fpga-specs/", "term/tri-fpga-tools/", "term/tri-fpga-wire/", "term/tri-fpga-batch/", "term/tri-fpga-status/", "term/tri-fpga-tmpcheck/", "term/tri-fpga-keycheck/", "term/tri-fpga-selftest/", "term/tri-x7-refs/", "term/tri-x7-audit/", "term/t27c-clock-domain/", "term/t27c-clocks/", "term/t27c-wukong-v1/", "term/t27c-cts/", "term/t27c-mmcm/", "term/t27c-mmcm-mutate/", "term/t27c-timing/", "term/t27c-reset-sync/", "term/t27c-reset-sync-gen/", "term/t27c-mtbf/", "term/t27c-clock-domain-gen/", "term/t27c-fifo/", "term/t27c-fifo-mutate/", "term/t27c-fifo-flags/", "term/t27c-emitter-xdc/", "term/t27c-fpga-rxcap/", "term/t27-tri-competitors/", "term/t27-tri-discard/", "term/tri-fpga-txhold/", "term/tri-fpga-rxcap/", "term/tri-fpga-rxer/", "term/tri-fpga-steps/", "term/tri-fpga-ioclients/", "term/tri-fpga-claim/", "term/tri-fpga-release/", "term/tri-fpga-next/", "term/t27c-memory/", "term/t27c-spi-tb/", "term/t27c-check-uart/", "term/t27c-check-spi/", "term/t27c-check-apb/", "term/t27c-check-axi4/", "term/t27c-check-bridge/", "term/t27c-check-memory/", "term/t27c-verilog-uart/", "term/t27c-verilog-spi/", "term/t27c-verilog-apb/", "term/t27c-verilog-axi4/", "term/t27c-verilog-bridge/", "term/t27c-verilog-top/", "term/t27c-verilog-uart-tb/", "term/t27c-verilog-apb-tb/", "term/t27c-verilog-axi-tb/", "term/t27c-verilog-mem-tb/", "term/t27c-verilog-integration-tb/", "term/t27c-verilog-top-tb/", "term/t27c-verilog-spi-tb/", "term/t27c-hir-uart/", "term/t27c-hir-apb/", "term/t27c-hir-axi4/", "term/t27c-eth-crc/", "term/t27c-rgmii/", "widgets/jtag-verdict/", "widgets/line-bounce/", "widgets/eye-isi/", "widgets/ecc-secded/", "widgets/carry-chain/", "widgets/prefix-adder/", "widgets/csa-tree/", "widgets/shift-add/", "widgets/booth-radix4/", "widgets/dsp-fit/", "widgets/q-format/", "widgets/round-sat/", "widgets/sqnr/", "widgets/cordic/", "widgets/cordic-vector/", "widgets/sine-table/", "widgets/alias-fold/", "widgets/nco/", "widgets/dft-bin/", "widgets/fir-response/", "widgets/fir-design/", "widgets/fir-cost/", "widgets/decimator/", "widgets/cic-growth/", "widgets/polyphase/", "widgets/fft-butterfly/", "widgets/bit-reverse/", "widgets/fft-scaling/", "widgets/barker-corr/", "widgets/fir-fit/", "widgets/fir-on-board/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "widgets/test-touch/", "widgets/t27c-stages/", "term/t27c-e8m0/", "play/e8m0/", "term/t27c-ocp-mx/", "play/gfternary/", "term/t27c-gfternary/", "play/golden-sieve/", "term/t27c-golden-sieve/", "play/tnf17/", "term/t27c-tnf17/", "term/t27c-activation-quantizer/", "term/t27c-bitnet-majority/", "term/t27c-bitnet-neuron-nchunk/", "term/t27c-comb-ternary-dot/", "term/t27c-stream-ternary-mac/", "term/t27c-bitnet-mlp/", "term/t27c-gft-smul/", "term/t27c-gft-sadd/", "term/t27c-gft-signed-mac/", "term/t27c-gft-relu/", "term/t27c-gft-exp2/", "term/t27c-gft-argmax4/", "term/t27c-gft-nll/", "term/t27c-gft-sgd-step/", "term/t27c-gft-xornet/", "widgets/basics-what-a-spec-is/", "widgets/basics-the-module-line/", "widgets/basics-comments-and-prose/", "widgets/basics-constants/", "widgets/basics-whole-numbers/", "widgets/basics-true-false-and-text/", "widgets/basics-arrays/", "widgets/basics-trits/", "widgets/basics-expressions/", "widgets/basics-a-test-block/", "widgets/basics-many-tests/", "widgets/basics-functions/", "widgets/basics-local-names/", "widgets/basics-if-and-else/", "widgets/basics-loops/", "widgets/basics-pub-or-private/", "widgets/basics-structs/", "widgets/basics-enums/", "widgets/basics-use-other-modules/", "widgets/basics-invariants/", "widgets/basics-gen-ts/", "widgets/basics-seven-backends/", "widgets/basics-stages-of-t27c/", "widgets/basics-tri-commands/", "widgets/basics-reading-errors/", "widgets/basics-a-small-program/", "widgets/basics-where-next/", "widgets/golden-split/", "widgets/golden-phi-ratio/", "widgets/golden-lucas/", "widgets/golden-split-optimality/", "widgets/golden-radix-economy/", "widgets/golden-phi-in-gf16/", "widgets/golden-gf4/", "widgets/golden-gf6/", "widgets/golden-gf8/", "widgets/golden-gf10/", "widgets/golden-gf12/", "widgets/golden-gf14/", "widgets/golden-gf16/", "widgets/golden-dot-two/", "widgets/golden-gf24/", "widgets/golden-gf32/", "widgets/golden-gf48/", "widgets/golden-gf64/", "widgets/golden-gf96/", "widgets/golden-gf128/", "widgets/golden-gf256/", "widgets/golden-gf512/", "widgets/golden-gf1024/", "widgets/golden-gft8/", "widgets/golden-gft16/", "widgets/golden-gft32/", "widgets/golden-decode/", "term/t27-tri-vectors/", "term/t27-tri-census/", "term/t27-tri-seals/", "term/t27-tri-prose/", "term/t27-tri-lean/", "term/tri-selftest/", "term/play-terminal-gates/", "widgets/tc-why-three/", "widgets/tc-balanced-count/", "widgets/tc-flip-and-cut/", "widgets/tc-kleene-gates/", "widgets/tc-consensus-any/", "widgets/tc-sum-and-carry/", "widgets/tc-half-adder/", "widgets/tc-full-adder/", "widgets/tc-ripple-carry/", "widgets/tc-times-a-trit/", "widgets/tc-long-multiply/", "widgets/tc-ternary-mac/", "widgets/tc-two-bits/", "widgets/tc-five-in-a-byte/", "widgets/tc-trit-word/", "widgets/tc-instruction-word/", "widgets/tc-opcode-map/", "widgets/tc-immediate/", "widgets/tc-ternary-alu/", "widgets/tc-three-way-jump/", "widgets/tc-run-a-program/", "widgets/tc-compare/", "widgets/tc-thirds/", "widgets/tc-sort-three/", "widgets/tc-one-neuron/", "widgets/tc-detector/", "widgets/tc-tiny-layer/", "widgets/nn-relu/", "widgets/nn-sigmoid/", "widgets/nn-tanh/", "widgets/nn-gelu/", "widgets/nn-silu/", "widgets/nn-elu/", "widgets/nn-softmax/", "widgets/nn-embedding/", "widgets/nn-dropout/", "widgets/nn-conv/", "widgets/nn-maxpool/", "widgets/nn-batchnorm/", "widgets/nn-mse/", "widgets/nn-bce/", "widgets/nn-huber/", "widgets/nn-sgd/", "widgets/nn-rmsprop/", "widgets/nn-adam/", "widgets/nn-backprop/", "widgets/nn-xor/", "widgets/nn-lamb/", "widgets/nn-rnn/", "widgets/nn-lstm/", "widgets/nn-attention/", "widgets/nn-positions/", "widgets/nn-encoder/", "widgets/nn-ppo/", "widgets/hive-shape/", "widgets/hive-boundary/", "widgets/hive-task-filter/", "widgets/hive-aging/", "widgets/hive-rank/", "widgets/hive-review-first/", "widgets/hive-lifecycle/", "widgets/hive-ship/", "widgets/hive-drain/", "widgets/hive-reviewers/", "widgets/hive-revisit/", "widgets/hive-gate/", "widgets/hive-jam/", "widgets/hive-jam-action/", "widgets/hive-master-red/", "widgets/hive-key-error/", "widgets/hive-key-rest/", "widgets/hive-next-key/", "widgets/hive-credit/", "widgets/hive-price/", "widgets/hive-burn/", "widgets/hive-called/", "widgets/hive-chain/", "widgets/hive-cooldown/", "widgets/hive-night/", "widgets/hive-column/", "widgets/hive-cell/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/", "widgets/race-the-bee/", "widgets/gatle/", "term/tri-blog-list/", "term/tri-game-selftest/"]; +pub const WIDGET_TITLES : [306]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", "", "", "", "", ""]; +pub const WIDGET_LINES : [306]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", "", "", "", "", ""]; ; Why a person would pass this one on: the reason the widget is in the gallery at all. -pub const WIDGET_HOOKS : [279]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "17 of 17 conformance specs compile, their test blocks pass, and no generated file has drifted.", "14 of 14 bench tools pass their own self-tests in one sweep: decode 18/18, repin 17/17, firelog 10/10, tmpcheck 9/9, stamps 9/9, keep 9/9.", "Requested 115,200 puts 115,385 on the wire (+0.16 %), 921,600 puts 923,077, and 3,000,000 is exact.", "26 checks, no board, no RTL: every receipt returns, 11 loss and fault cases recover, and a lie stops the run.", "One command reads the bench: cables, locks, and the last board runs: three x7-board runs PASS 51840/51840.", "112 files under /tmp pinned by sha256 in specs: every one has a kept copy, 0 lost, 0 unlocated.", "84 board and model logs scanned for the TRI-NET key: 0 hits, 0 logs of 1 MB or more.", "16 spec-named scripts run their own --self-test, 0 bad; 8 without the flag are listed, not skipped silently.", "The part table in far.t27 matches prjxray-db part.json, all 6 reference bitstreams present, bitwalk reads all 6 in 84.4 ms.", "One command re-runs every sweep: CRC 12/12, ECC 32520/32520 frames, 82/82 pins, 4224/4224 segbits, gold 5/5. audit: CLEAN.", "The CDC spec through the real compiler: 12 tests pass natively and the header says what the model does and does not claim.", "The tri clocks card as a spec: constants a tool page reads, and a native test-report that says so plainly -- 0 tests, because it is not a test suite.", "The board itself as a spec: 15 tests pass natively and the part line names the xc7a200tfgg676 the Wukong carries.", "Clock-tree synthesis in numbers: 15 tests pass natively, and the header pins the Artix-7 teaching delays the tree arithmetic uses.", "The MMCM model through the real compiler: VCO window, M/D/O ranges, fractional multiply in eighths, the 1/56 phase step, 16 tests natively.", "Widen the VCO window with one sed and exactly one test objects -- the rejection test that names the 500 MHz config it calls out.", "Static timing as arithmetic: 25 tests pass natively on arcs, slack and Fmax, with the Artix-7 teaching delays in the header.", "The three reset kinds through the real compiler: async assert with sync release through 2 to 4 flops, and the racy async release, 16 tests natively.", "What the compiler emits for the reset pipe: t27c gen prints the Zig, one struct copy per clock, and wc -l counts it.", "Metastability MTBF without floats: the WP323 form in Q10 log2, one flop negative slack, two flops astronomical, 15 tests natively.", "The CDC model as Zig: t27c gen prints what it compiled and wc -l says how many lines of code the model is.", "The FIFO stdlib through the real compiler: 18 tests pass natively and 6 invariants are proved comptime.", "Move a pointer by 3 instead of 1 and every test still passes: the suite asserts fill and flags, never pointer values.", "Flip one flag assignment and a single test objects: the guard that keeps the FIFO honest.", "The pins-to-XDC emitter spec parsed by the real compiler, and the create_clock lines it exists to write.", "The fpga-rxcap tool card as a spec: constants for a tool page, and a native run that says 0 tests and why.", "168 records: 14 papers entered twice, 141 cite no score, 144 state zero at pass@1.", "67 specs lose 23,578 tokens to parser recovery, each held at its pinned bound.", "Smallest hold 3445 ps on TXD1 against a 1000 ps bench; each TX pin setup and hold from the routed SDF.", "Per-bit setup and hold of an async RX nibble against its RXC edge, from the routed SDF: one nibble changes 2387 to 2486 ps apart, so a binary counter tears where a gray one cannot.", "RX_ER pulses injected at exact nibble positions of a ping -- preamble, mid-frame, IP header -- and the frame state each pulse leaves behind, RTL vs model.", "The Ethernet bring-up plan on the bench: pre-registered steps, logged exits, what ran and what did not.", "The registered IO clients on the bench, read live: names, claims and what each declares it owns.", "The bench claim in action: one client takes the hardware lock and the tool reports who holds it.", "The bench lock given back: the client gives up its claim and the bench reports itself free.", "The next queued step on the bench, read live from the run state.", "The memory map spec through the real compiler: 15 tests pass natively and 6 invariants are proved comptime.", "The SPI testbench through the real compiler: 7 tests pass natively, driving the modes and checking the captures.", "t27c check on the UART spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the SPI spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the APB bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the AXI4 spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the bus bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the memory spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "The UART spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The SPI spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The APB bridge spec lowered to synthesizable Verilog: port list, PSEL and PENABLE wiring, the address decode.", "The AXI4 spec lowered to synthesizable Verilog: the five channels' wiring and handshakes as generated wires.", "The bus bridge spec lowered to synthesizable Verilog: both sides of the crossing as generated wires.", "The whole design in one spec lowered to synthesizable Verilog: clocks, resets and bus wiring as the compiler emits them.", "The UART testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The APB bridge testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The AXI4 testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The memory testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The integration testbench spec lowered to Verilog: every bus of the design in one bench.", "The top-level testbench spec lowered to Verilog: the whole design under one bench.", "The SPI testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The UART spec lowered to hardware IR: the compiler's own view of the wires and state before Verilog exists.", "The APB bridge spec lowered to hardware IR: PSEL, PENABLE and the decode as the compiler structures them.", "The AXI4 spec lowered to hardware IR: the five channels and their handshakes as the compiler structures them.", "The Ethernet FCS spec through the real compiler: 8 tests pass natively -- the table derived from one bit step, the 0xCBF43926 check value, the 0xDEBB20E3 receiver residue.", "The RGMII spec through the real compiler: 7 tests pass natively and 1 invariant is proved comptime -- the 8 ns gigabit period, a nibble on both edges, the 78 ps tap ladder.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "", "", "The OCP MX scale byte as a spec: 2^(e-127), code 255 is NaN, 18 tests and 9 comptime invariants, compiled on a real machine.", "The same scale byte compiled in your browser: seven backends, the spec's tests run in the page, and every skip says why.", "One outlier sets the scale for 32 weights: MXFP4 flushes 23 of them to zero, MXFP8 E4M3 none. 49 tests pass; a planted bug fails one.", "Ternary weights scaled by phi, compiled in your browser: codes 00, 01, 10, and the fourth code folded to zero.", "13 of 13 tests pass natively, and the report says 8 of them executed no assert; a planted bug fails exactly one.", "Five measured rules for a ternary weight alphabet, and the one formula that passes all five, run in your browser.", "One more trit and the spec does not compile at all: BLOCKED, not failed, because no test ever ran.", "A 17-bit float whose exponent is four balanced trits, 3^4 = 81 values, compiled in your browser.", "34 tests pass natively, none vacuous; swap one XOR for OR and only the test that negates twice fails.", "A neuron's sum squeezed back to one trit: 7 tests pass natively, and turning one > into >= fails only the boundary test.", "One ternary neuron, two functions picked by its weights: 12 tests pass natively, and one weight moved from P to Z fails exactly one.", "A neuron wider than 27 inputs, summed chunk by chunk: 11 tests pass natively, and a loop that stops after one chunk fails exactly one.", "A 27-lane ternary dot product as pure wires: 4 tests pass natively, and one wrong sign in tmul fails exactly one.", "The same dot product added into a register on every clock: 4 tests pass natively, and a lane mask short one bit fails exactly one.", "A whole small ternary network, 3 neurons then 2: 4 tests pass natively, and a trit packed one bit low fails exactly one.", "A signed multiply: 3 tests pass natively, none vacuous, and a sign stuck at 0 fails exactly one, m2.", "A signed add: 3 tests pass natively, and adding where opposite signs must subtract fails exactly one, a2.", "Two signed products summed: 4 tests pass natively, and OR in place of the sign XOR fails exactly one, pp.", "A ReLU: 4 tests pass natively, and letting negative inputs through fails only negz, the one test with a negative input.", "2^x for a softmax: 4 tests pass natively, and dropping the minus sign of k fails exactly one, em1.", "argmax by comparing bits alone: 4 tests pass natively, and >= in place of > fails exactly one, tie_low.", "A loss in bits: 3 tests pass natively, and a zero with the sign bit set fails exactly one, perfect.", "One SGD step: 3 tests pass natively, and g times g in place of eta times g fails exactly one, ascend.", "A 2-layer ReLU net for XOR: 4 tests pass natively, and one relu removed fails exactly one, x00.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "34 vector files, 512 cases: 2 files executed, 8 debt, 24 prose-only; 19% carry data.", "Four census populations recounted another way all agree; three are named as uncheckable.", "31 of 1,428 seals record no output (16 specs); 3 of those are missing from the debt ledger.", "11 specs refused at parse, 0 blocked only by prose, 23 broken on purpose as fixtures.", "12 Lean files, 15,553 lines and 4 sorry, are reached by no build root: nothing compiles them.", "tri, railway, python3 and the loop directory: each present or not, in one call.", "The terminal for seven t27 backends: every command run on hello_world. 21 pass, 0 fail.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters.", "", "", "73 published posts with date, language and revision, newest first.", "6 of 6 self-tests hold: anomalies, blog_drift, lessons, site_gate, tick, vault."]; +pub const WIDGET_HOOKS : [306]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "17 of 17 conformance specs compile, their test blocks pass, and no generated file has drifted.", "14 of 14 bench tools pass their own self-tests in one sweep: decode 18/18, repin 17/17, firelog 10/10, tmpcheck 9/9, stamps 9/9, keep 9/9.", "Requested 115,200 puts 115,385 on the wire (+0.16 %), 921,600 puts 923,077, and 3,000,000 is exact.", "26 checks, no board, no RTL: every receipt returns, 11 loss and fault cases recover, and a lie stops the run.", "One command reads the bench: cables, locks, and the last board runs: three x7-board runs PASS 51840/51840.", "112 files under /tmp pinned by sha256 in specs: every one has a kept copy, 0 lost, 0 unlocated.", "84 board and model logs scanned for the TRI-NET key: 0 hits, 0 logs of 1 MB or more.", "16 spec-named scripts run their own --self-test, 0 bad; 8 without the flag are listed, not skipped silently.", "The part table in far.t27 matches prjxray-db part.json, all 6 reference bitstreams present, bitwalk reads all 6 in 84.4 ms.", "One command re-runs every sweep: CRC 12/12, ECC 32520/32520 frames, 82/82 pins, 4224/4224 segbits, gold 5/5. audit: CLEAN.", "The CDC spec through the real compiler: 12 tests pass natively and the header says what the model does and does not claim.", "The tri clocks card as a spec: constants a tool page reads, and a native test-report that says so plainly -- 0 tests, because it is not a test suite.", "The board itself as a spec: 15 tests pass natively and the part line names the xc7a200tfgg676 the Wukong carries.", "Clock-tree synthesis in numbers: 15 tests pass natively, and the header pins the Artix-7 teaching delays the tree arithmetic uses.", "The MMCM model through the real compiler: VCO window, M/D/O ranges, fractional multiply in eighths, the 1/56 phase step, 16 tests natively.", "Widen the VCO window with one sed and exactly one test objects -- the rejection test that names the 500 MHz config it calls out.", "Static timing as arithmetic: 25 tests pass natively on arcs, slack and Fmax, with the Artix-7 teaching delays in the header.", "The three reset kinds through the real compiler: async assert with sync release through 2 to 4 flops, and the racy async release, 16 tests natively.", "What the compiler emits for the reset pipe: t27c gen prints the Zig, one struct copy per clock, and wc -l counts it.", "Metastability MTBF without floats: the WP323 form in Q10 log2, one flop negative slack, two flops astronomical, 15 tests natively.", "The CDC model as Zig: t27c gen prints what it compiled and wc -l says how many lines of code the model is.", "The FIFO stdlib through the real compiler: 18 tests pass natively and 6 invariants are proved comptime.", "Move a pointer by 3 instead of 1 and every test still passes: the suite asserts fill and flags, never pointer values.", "Flip one flag assignment and a single test objects: the guard that keeps the FIFO honest.", "The pins-to-XDC emitter spec parsed by the real compiler, and the create_clock lines it exists to write.", "The fpga-rxcap tool card as a spec: constants for a tool page, and a native run that says 0 tests and why.", "168 records: 14 papers entered twice, 141 cite no score, 144 state zero at pass@1.", "67 specs lose 23,578 tokens to parser recovery, each held at its pinned bound.", "Smallest hold 3445 ps on TXD1 against a 1000 ps bench; each TX pin setup and hold from the routed SDF.", "Per-bit setup and hold of an async RX nibble against its RXC edge, from the routed SDF: one nibble changes 2387 to 2486 ps apart, so a binary counter tears where a gray one cannot.", "RX_ER pulses injected at exact nibble positions of a ping -- preamble, mid-frame, IP header -- and the frame state each pulse leaves behind, RTL vs model.", "The Ethernet bring-up plan on the bench: pre-registered steps, logged exits, what ran and what did not.", "The registered IO clients on the bench, read live: names, claims and what each declares it owns.", "The bench claim in action: one client takes the hardware lock and the tool reports who holds it.", "The bench lock given back: the client gives up its claim and the bench reports itself free.", "The next queued step on the bench, read live from the run state.", "The memory map spec through the real compiler: 15 tests pass natively and 6 invariants are proved comptime.", "The SPI testbench through the real compiler: 7 tests pass natively, driving the modes and checking the captures.", "t27c check on the UART spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the SPI spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the APB bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the AXI4 spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the bus bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the memory spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "The UART spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The SPI spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The APB bridge spec lowered to synthesizable Verilog: port list, PSEL and PENABLE wiring, the address decode.", "The AXI4 spec lowered to synthesizable Verilog: the five channels' wiring and handshakes as generated wires.", "The bus bridge spec lowered to synthesizable Verilog: both sides of the crossing as generated wires.", "The whole design in one spec lowered to synthesizable Verilog: clocks, resets and bus wiring as the compiler emits them.", "The UART testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The APB bridge testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The AXI4 testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The memory testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The integration testbench spec lowered to Verilog: every bus of the design in one bench.", "The top-level testbench spec lowered to Verilog: the whole design under one bench.", "The SPI testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The UART spec lowered to hardware IR: the compiler's own view of the wires and state before Verilog exists.", "The APB bridge spec lowered to hardware IR: PSEL, PENABLE and the decode as the compiler structures them.", "The AXI4 spec lowered to hardware IR: the five channels and their handshakes as the compiler structures them.", "The Ethernet FCS spec through the real compiler: 8 tests pass natively -- the table derived from one bit step, the 0xCBF43926 check value, the 0xDEBB20E3 receiver residue.", "The RGMII spec through the real compiler: 7 tests pass natively and 1 invariant is proved comptime -- the 8 ns gigabit period, a nibble on both edges, the 78 ps tap ladder.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "", "", "The OCP MX scale byte as a spec: 2^(e-127), code 255 is NaN, 18 tests and 9 comptime invariants, compiled on a real machine.", "The same scale byte compiled in your browser: seven backends, the spec's tests run in the page, and every skip says why.", "One outlier sets the scale for 32 weights: MXFP4 flushes 23 of them to zero, MXFP8 E4M3 none. 49 tests pass; a planted bug fails one.", "Ternary weights scaled by phi, compiled in your browser: codes 00, 01, 10, and the fourth code folded to zero.", "13 of 13 tests pass natively, and the report says 8 of them executed no assert; a planted bug fails exactly one.", "Five measured rules for a ternary weight alphabet, and the one formula that passes all five, run in your browser.", "One more trit and the spec does not compile at all: BLOCKED, not failed, because no test ever ran.", "A 17-bit float whose exponent is four balanced trits, 3^4 = 81 values, compiled in your browser.", "34 tests pass natively, none vacuous; swap one XOR for OR and only the test that negates twice fails.", "A neuron's sum squeezed back to one trit: 7 tests pass natively, and turning one > into >= fails only the boundary test.", "One ternary neuron, two functions picked by its weights: 12 tests pass natively, and one weight moved from P to Z fails exactly one.", "A neuron wider than 27 inputs, summed chunk by chunk: 11 tests pass natively, and a loop that stops after one chunk fails exactly one.", "A 27-lane ternary dot product as pure wires: 4 tests pass natively, and one wrong sign in tmul fails exactly one.", "The same dot product added into a register on every clock: 4 tests pass natively, and a lane mask short one bit fails exactly one.", "A whole small ternary network, 3 neurons then 2: 4 tests pass natively, and a trit packed one bit low fails exactly one.", "A signed multiply: 3 tests pass natively, none vacuous, and a sign stuck at 0 fails exactly one, m2.", "A signed add: 3 tests pass natively, and adding where opposite signs must subtract fails exactly one, a2.", "Two signed products summed: 4 tests pass natively, and OR in place of the sign XOR fails exactly one, pp.", "A ReLU: 4 tests pass natively, and letting negative inputs through fails only negz, the one test with a negative input.", "2^x for a softmax: 4 tests pass natively, and dropping the minus sign of k fails exactly one, em1.", "argmax by comparing bits alone: 4 tests pass natively, and >= in place of > fails exactly one, tie_low.", "A loss in bits: 3 tests pass natively, and a zero with the sign bit set fails exactly one, perfect.", "One SGD step: 3 tests pass natively, and g times g in place of eta times g fails exactly one, ascend.", "A 2-layer ReLU net for XOR: 4 tests pass natively, and one relu removed fails exactly one, x00.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "34 vector files, 512 cases: 2 files executed, 8 debt, 24 prose-only; 19% carry data.", "Four census populations recounted another way all agree; three are named as uncheckable.", "31 of 1,428 seals record no output (16 specs); 3 of those are missing from the debt ledger.", "11 specs refused at parse, 0 blocked only by prose, 23 broken on purpose as fixtures.", "12 Lean files, 15,553 lines and 4 sorry, are reached by no build root: nothing compiles them.", "tri, railway, python3 and the loop directory: each present or not, in one call.", "The terminal for seven t27 backends: every command run on hello_world. 21 pass, 0 fail.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters.", "", "", "73 published posts with date, language and revision, newest first.", "6 of 6 self-tests hold: anomalies, blog_drift, lessons, site_gate, tick, vault."]; ; --- Ideas: asked for, not built --------------------------------------------------------------- ; From what engineers share today (gh-skyline, Tiny Tapeout renders, Wrapped cards, Gatle-style @@ -119,9 +119,9 @@ test the_fpga_shelf_comes_first_and_opens_on_the_board { } test the_arena_is_a_tab_and_says_its_own_words { - assert WIDGET_IDS[271] == "wars"; - assert WIDGET_KINDS[271] == "tab"; - assert WIDGET_PAGES[271] == "wars"; + assert WIDGET_IDS[298] == "wars"; + assert WIDGET_KINDS[298] == "tab"; + assert WIDGET_PAGES[298] == "wars"; } test the_gallery_posts_nothing { @@ -157,8 +157,8 @@ test the_verification_shelf_is_rehearsals_and_audits { test ideas_are_counted_apart_from_widgets { assert IDEA_COUNT == 6; - assert WIDGET_IDS[276] == "gatle"; - assert WIDGET_COUNT == 279; + assert WIDGET_IDS[303] == "gatle"; + assert WIDGET_COUNT == 306; } test the_ternary_machine_shelf_runs_before_the_arena { @@ -172,9 +172,9 @@ test every_tool_lives_under_widgets_and_keeps_its_words_on_its_page { assert WIDGET_KINDS[9] == "tool"; assert WIDGET_PAGES[9] == "widgets/bit-diff/"; assert WIDGET_HOOKS[9] == ""; - assert WIDGET_KINDS[275] == "tool"; - assert WIDGET_CATEGORIES[275] == "game"; - assert WIDGET_PAGES[275] == "widgets/race-the-bee/"; + assert WIDGET_KINDS[302] == "tool"; + assert WIDGET_CATEGORIES[302] == "game"; + assert WIDGET_PAGES[302] == "widgets/race-the-bee/"; } test the_arithmetic_shelf_closes_the_fpga_bench { @@ -192,5 +192,14 @@ test the_neural_network_shelf_runs_before_the_arena { assert WIDGET_PAGES[244] == "widgets/nn-relu/"; assert WIDGET_IDS[270] == "nn-ppo"; assert WIDGET_CATEGORIES[270] == "compiler"; - assert WIDGET_IDS[271] == "wars"; + assert WIDGET_IDS[298] == "wars"; +} + +test the_hive_shelf_runs_before_the_arena { + assert WIDGET_IDS[271] == "hive-shape"; + assert WIDGET_KINDS[271] == "tool"; + assert WIDGET_PAGES[271] == "widgets/hive-shape/"; + assert WIDGET_IDS[297] == "hive-cell"; + assert WIDGET_CATEGORIES[297] == "game"; + assert WIDGET_IDS[298] == "wars"; } diff --git a/apps/website/public/widgets/hive-aging/card.png b/apps/website/public/widgets/hive-aging/card.png new file mode 100644 index 0000000000..88b3ab5c26 Binary files /dev/null and b/apps/website/public/widgets/hive-aging/card.png differ diff --git a/apps/website/public/widgets/hive-aging/hive-aging.t27 b/apps/website/public/widgets/hive-aging/hive-aging.t27 new file mode 100644 index 0000000000..f2eea305d7 --- /dev/null +++ b/apps/website/public/widgets/hive-aging/hive-aging.t27 @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-aging.t27 -- Aging: how long a labelled low issue waits +; Source of truth for public/widgets/hive-aging/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-aging/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/priority.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 4 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a level, whether it is labelled, and an age from 0 to 60 days. The +; green line is effective_level over every day for a labelled issue and the dashed one for an +; unlabelled one; the tiles read it and why at the chosen age. + +; WHAT IT DOES NOT CLAIM: it does not read GitHub labels: the level is what the reader picks. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_aging; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-aging"; +pub const TITLE : str = "Aging: how long a labelled low issue waits"; +pub const DESCRIPTION : str = "Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical."; +pub const IMAGE_ALT : str = "Aging card: a labelled LOW issue at 20 days. The green step line runs at low until day 14 and then at normal to day 60; the dashed unlabelled line stays flat. Tiles read effective level normal, why aged, 14 days a step."; +pub const HOOK : str = "A low label that aged past unlabelled work would make low the fastest way to the front. So aging is one step, and only for labelled issues."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is effective_level and why, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 4 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_AGE_MAX : u8 = 60; +pub const K_AGE_TICKS : [5]u8 = [0, 14, 28, 42, 60]; +pub const K_AGING_DAYS : u32 = 14; +pub const K_MAX_AGING_STEPS : u32 = 1; +pub const K_PRESET_LEVEL : [4]u8 = [3, 2, 3, 1]; +pub const K_PRESET_LABELLED : [4]u8 = [1, 1, 0, 1]; +pub const K_PRESET_AGE : [4]u8 = [20, 30, 50, 45]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Low, 20 days", "Normal, 30 days", "Low, no label", "High, 45 days"]; +pub const SAY_ASK : str = "How long does a labelled low issue wait before it moves up?"; +pub const SAY_LEVEL : str = "level"; +pub const SAY_LEVEL_NAMES : [4]str = ["critical", "high", "normal", "low"]; +pub const SAY_LABELLED : str = "labelled"; +pub const SAY_AGE : str = "days open"; +pub const SAY_PLOT_LABEL : str = "the level an issue runs at, by days open"; +pub const SAY_LINE : str = "labelled"; +pub const SAY_FLAT : str = "not labelled"; +pub const SAY_PICKED : str = "level picked"; +pub const SAY_EFFECTIVE : str = "runs at"; +pub const SAY_STEP : str = "days a step"; +pub const SAY_WHY : str = "why"; +pub const SAY_WHY_NAMES : [5]str = ["listing", "label", "aged", "capped", "blocked"]; +pub const SAY_NOTE : str = "Lower runs first. An unlabelled issue is normal and never ages, so a repository with no labels keeps the order it had. High and critical do not age at all."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/priority.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test one_step_every_fourteen_days { + assert K_AGING_DAYS == 14; + assert K_MAX_AGING_STEPS == 1; + assert K_AGE_TICKS[1] == K_AGING_DAYS; +} + +test the_presets_fit_the_axis { + assert K_PRESET_AGE[2] <= K_AGE_MAX; + assert K_PRESET_LEVEL[0] == 3; + assert K_AGE_TICKS[4] == K_AGE_MAX; +} diff --git a/apps/website/public/widgets/hive-aging/index.html b/apps/website/public/widgets/hive-aging/index.html new file mode 100644 index 0000000000..cc48edcb67 --- /dev/null +++ b/apps/website/public/widgets/hive-aging/index.html @@ -0,0 +1,47 @@ + + + + + + +Aging: how long a labelled low issue waits · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Aging: how long a labelled low issue waits

+

Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is effective_level and why, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 4 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-aging/logic.js b/apps/website/public/widgets/hive-aging/logic.js new file mode 100644 index 0000000000..50d377cb05 --- /dev/null +++ b/apps/website/public/widgets/hive-aging/logic.js @@ -0,0 +1,40 @@ +// Generated by scripts/t27-logic.mjs from priority.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: d5d5e42e10e89055810a8e8115285f6af4683f8c6e6c36501fa2b4419f4ed070 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-aging/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJgVgBX9/f39/AX9gBn9/f39/fwF/YAF/AX9gA39/fwF/YAJ/fwF/AwYFAAECAwQEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdbBgZtZW1vcnkCAAd0Mjdfd2h5AAAMdDI3X291dHJhbmtzAAEMdDI3X2VsaWdpYmxlAAITdDI3X2VmZmVjdGl2ZV9sZXZlbAADEHQyN19jYXBwZWRfbGV2ZWwABAqyAQUnAAJAIARFDQBBBA8LAkAgASAARg0AQQMPCyADQQFxQQIgAiABRhsLNwEBf0EBIQYCQCAAIANJDQBBACEGIAAgA0sNAEEBIQYgASAESQ0AIAEgBE0gAiAFSXEhBgsgBgsFACAARQs9AAJAAkAgAUEBcUUNACAAQQJJDQBBASEBIABBf2ogAkENSyICTQ0BIAAgAmshAQwBCyAAIQELIAFB/wFxCwwAIAAgAUECSyAAGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function cappedLevel(base, criticalsBefore) { + return W.t27_capped_level(int(base, 0, 255, "capped_level.base"), int(criticalsBefore, 0, 4294967295, "capped_level.criticals_before")) & 255; +} +function effectiveLevel(level, labelled, ageDays) { + return W.t27_effective_level(int(level, 0, 255, "effective_level.level"), labelled ? 1 : 0, int(ageDays, 0, 4294967295, "effective_level.age_days")) & 255; +} +function eligible(openBlockers) { + return (W.t27_eligible(int(openBlockers, 0, 4294967295, "eligible.open_blockers")) & 1) !== 0; +} +function outranks(effA, levelA, indexA, effB, levelB, indexB) { + return (W.t27_outranks(int(effA, 0, 255, "outranks.eff_a"), int(levelA, 0, 255, "outranks.level_a"), int(indexA, 0, 4294967295, "outranks.index_a"), int(effB, 0, 255, "outranks.eff_b"), int(levelB, 0, 255, "outranks.level_b"), int(indexB, 0, 4294967295, "outranks.index_b")) & 1) !== 0; +} +function why(base, level, eff, labelled, openBlockers) { + return W.t27_why(int(base, 0, 255, "why.base"), int(level, 0, 255, "why.level"), int(eff, 0, 255, "why.eff"), labelled ? 1 : 0, int(openBlockers, 0, 4294967295, "why.open_blockers")) & 255; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + cappedLevel, + effectiveLevel, + eligible, + outranks, + why +}; diff --git a/apps/website/public/widgets/hive-aging/logic.t27 b/apps/website/public/widgets/hive-aging/logic.t27 new file mode 100644 index 0000000000..e0a7399a68 --- /dev/null +++ b/apps/website/public/widgets/hive-aging/logic.t27 @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366) +; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no +; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it. +; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js` +; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes. +; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank, +; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort): +; - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first); +; - an issue with an open blocker is skipped, not waited on (head-of-line blocking); +; - a cap on critical stops priority inflation; aging stops starvation of labelled low work; +; - an issue nobody labelled keeps today's place, so a repository with no labels sees no change. +; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority +; field (read it first once the org enables it; its four values map onto the same levels). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenPriority; + +/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages. +pub const LEVEL_CRITICAL : u8 = 0; +pub const LEVEL_HIGH : u8 = 1; +pub const LEVEL_NORMAL : u8 = 2; +pub const LEVEL_LOW : u8 = 3; + +/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing. +/// When an issue carries labels of two levels, the more urgent one counts. +pub const CRITICAL_LABELS : str = "P0,priority/critical,critical"; +pub const HIGH_LABELS : str = "P1,priority/high,high-priority"; +pub const NORMAL_LABELS : str = "P2,priority/medium"; +pub const LOW_LABELS : str = "P3,P4,priority/low"; + +/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels, +/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low" +/// the fastest way to the front of a long queue. +pub const AGING_DAYS : u32 = 14; +pub const MAX_AGING_STEPS : u32 = 1; + +/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH. +pub const CRITICAL_CAP : u32 = 3; + +/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs. +pub const WHY_LISTING : u8 = 0; +pub const WHY_LABEL : u8 = 1; +pub const WHY_AGED : u8 = 2; +pub const WHY_CAPPED : u8 = 3; +pub const WHY_BLOCKED : u8 = 4; + +/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one. +pub fn capped_level(base: u8, criticals_before: u32) -> u8 { + if (base == LEVEL_CRITICAL) { + if (criticals_before >= CRITICAL_CAP) { + return LEVEL_HIGH; + } + } + return base; +} + +/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS) +/// when it is labelled. +pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 { + if (labelled == false) { + return level; + } + if (level <= LEVEL_HIGH) { + return level; + } + var steps : u32 = age_days / AGING_DAYS; + if (steps > MAX_AGING_STEPS) { + steps = MAX_AGING_STEPS; + } + var gap : u32 = (level as u32) - (LEVEL_HIGH as u32); + if (steps >= gap) { + return LEVEL_HIGH; + } + return ((level as u32) - steps) as u8; +} + +/// An issue with an open blocker is not a candidate; the Queen moves past it. +pub fn eligible(open_blockers: u32) -> bool { + return open_blockers == 0; +} + +/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged +/// issue never beats a real one of the same level), then earlier in GitHub's listing. +pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool { + if (eff_a < eff_b) { return true; } + if (eff_a > eff_b) { return false; } + if (level_a < level_b) { return true; } + if (level_a > level_b) { return false; } + return index_a < index_b; +} + +/// The reason printed for one issue. +pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 { + if (open_blockers > 0) { return WHY_BLOCKED; } + if (level != base) { return WHY_CAPPED; } + if (eff != level) { return WHY_AGED; } + if (labelled) { return WHY_LABEL; } + return WHY_LISTING; +} + +test no_label_keeps_the_listing_order { + assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1); + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false; + // #7077: a strict order, so a sort can use it: no issue outranks itself + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false; +} + +test critical_runs_before_a_newer_unlabelled_issue { + assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0); +} + +test unlabelled_never_beats_urgent { + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false; +} + +test labelled_work_ages_one_level_and_never_to_critical { + assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW; + assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL; + assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL; + assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH; + assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH; +} + +test an_aged_issue_loses_the_tie_to_a_real_one { + var aged : u8 = effective_level(LEVEL_LOW, true, 28); + assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0); + // #7077: of two LOW labels the aged one goes first, whatever the listing says + assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0); + assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false; +} + +test the_fourth_critical_runs_as_high { + assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL; + assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL; + assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH; + assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW; +} + +test a_blocked_issue_is_skipped { + assert eligible(0); + assert eligible(1) == false; + assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED; +} + +test every_reason_is_reachable { + assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING; + assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL; + assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED; + assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED; +} + +invariant levels_are_ordered { + assert LEVEL_CRITICAL < LEVEL_HIGH; + assert LEVEL_HIGH < LEVEL_NORMAL; + assert LEVEL_NORMAL < LEVEL_LOW; + assert CRITICAL_CAP > 0; + assert AGING_DAYS > 0; + assert MAX_AGING_STEPS == 1; +} diff --git a/apps/website/public/widgets/hive-aging/tool.js b/apps/website/public/widgets/hive-aging/tool.js new file mode 100644 index 0000000000..9fa9557a2a --- /dev/null +++ b/apps/website/public/widgets/hive-aging/tool.js @@ -0,0 +1,37 @@ +// hive-aging/tool.js -- lesson 4 of queen-and-hive: how long a labelled low issue waits. Every point +// of both lines and every tile is effective_level (and why) of gHashTag/t27 specs/queen/priority.t27, +// compiled to wasm (./logic.js). Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-aging.t27. +import { effectiveLevel, why } from './logic.js' +import { COLORS, K, W, ask, canvas, controls, el, plot, root, say, source, svgEl, tiles } from '../hive-kit.js' + +const AMAX = K('K_AGE_MAX', 60) +const ctl = controls([ + { key: 'level', label: say('SAY_LEVEL'), kind: 'choice', options: W.SAY_LEVEL_NAMES }, + { key: 'age', label: say('SAY_AGE'), min: 0, max: AMAX, step: 1 }, + { key: 'labelled', label: say('SAY_LABELLED'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { level: K('K_PRESET_LEVEL', []), labelled: K('K_PRESET_LABELLED', []), age: K('K_PRESET_AGE', []) } }, draw) + +const svg = canvas('hk-plot', say('SAY_PLOT_LABEL')) +const stat = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, stat, svg, el('p', { class: 'dk-legend' }, el('span', {}, el('i', { style: `background:${COLORS[0]}` }), say('SAY_LINE')), el('span', {}, el('i', { style: `background:${COLORS[2]};opacity:0.6` }), say('SAY_FLAT'))), el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + svg.replaceChildren() + const p = plot(svg, { w: 640, h: 300, x0: 0, x1: AMAX, y0: 3.4, y1: -0.4, xticks: K('K_AGE_TICKS', []), yticks: [0, 1, 2, 3], yfmt: (t) => say('SAY_LEVEL_NAMES', t), xlabel: say('SAY_AGE'), left: 78 }) + const days = [...Array(AMAX + 1)].map((_, d) => d) + const step = (f) => days.flatMap((d) => [[d, f(d)], [d + 1, f(d)]]).filter(([x]) => x <= AMAX) + p.line(step((d) => effectiveLevel(s.level, false, d)), COLORS[2], 2, { 'stroke-dasharray': '6 5' }) + p.line(step((d) => effectiveLevel(s.level, true, d)), COLORS[0], 4) + const eff = effectiveLevel(s.level, !!s.labelled, s.age) + svg.append(svgEl('circle', { cx: p.x(s.age), cy: p.y(eff), r: 8, fill: COLORS[1], stroke: '#020403', 'stroke-width': 3 })) + const w = why(s.level, s.level, eff, !!s.labelled, 0) + stat.replaceChildren(tiles([ + { label: say('SAY_PICKED'), value: say('SAY_LEVEL_NAMES', s.level), tone: 2 }, + { label: say('SAY_EFFECTIVE'), value: say('SAY_LEVEL_NAMES', eff), tone: 1 }, + { label: say('SAY_WHY'), value: say('SAY_WHY_NAMES', w), tone: 5 }, + { label: say('SAY_STEP'), value: String(K('K_AGING_DAYS', 14)), tone: 0 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-boundary/card.png b/apps/website/public/widgets/hive-boundary/card.png new file mode 100644 index 0000000000..bd0f4d06ff Binary files /dev/null and b/apps/website/public/widgets/hive-boundary/card.png differ diff --git a/apps/website/public/widgets/hive-boundary/hive-boundary.t27 b/apps/website/public/widgets/hive-boundary/hive-boundary.t27 new file mode 100644 index 0000000000..06b989b0a1 --- /dev/null +++ b/apps/website/public/widgets/hive-boundary/hive-boundary.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-boundary.t27 -- A boundary: which files a task may reserve +; Source of truth for public/widgets/hive-boundary/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-boundary/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/shaping.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 2 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: each of four paths carries four facts the reader taps: the file exists, its folder +; exists, the issue cites it, it would be a new file in a language the only-t27 rule denies. Each +; path is path_accepted of its facts; the gate is shape_lands(missing after, 4, refused). + +; WHAT IT DOES NOT CLAIM: it does not read a repository: the four paths and their facts are what +; the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_boundary; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-boundary"; +pub const TITLE : str = "A boundary: which files a task may reserve"; +pub const DESCRIPTION : str = "Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds."; +pub const IMAGE_ALT : str = "Boundary card: four proposed paths. An existing spec and a new cited t27 file in an existing folder are green; a cited file in a folder that does not exist and a new cited .py file are red. The gate is shut: 2 of 4 paths refused, the shape does not land."; +pub const HOOK : str = "A wrong boundary is worse than a missing one: it reserves files the work does not own. An invented path is refused, and one refusal stops the shape."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is path_accepted and shape_lands, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 2 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_PATHS : u8 = 4; +pub const K_MAX_BOUNDARY_PATHS : u32 = 12; +pub const K_FACT_BITS : [4]u8 = [1, 2, 4, 8]; +pub const K_PRESET_P0 : [4]u8 = [1, 1, 1, 1]; +pub const K_PRESET_P1 : [4]u8 = [6, 6, 6, 0]; +pub const K_PRESET_P2 : [4]u8 = [4, 6, 4, 4]; +pub const K_PRESET_P3 : [4]u8 = [14, 6, 14, 14]; +pub const K_PRESET_LEFT : [4]u8 = [0, 0, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Two invented paths", "Every path checked", "Still a section missing", "Only real files"]; +pub const SAY_ASK : str = "Which files may a task reserve before a bee starts on it?"; +pub const SAY_PATHS : [4]str = ["specs/queen/hive.t27", "specs/queen/comb.t27", "tools/queen/draw/new.t27", "tools/queen/draw.py"]; +pub const SAY_FACTS : [4]str = ["exists", "folder exists", "cited by the issue", "new, foreign language"]; +pub const SAY_LEFT : str = "a section still missing after the shape"; +pub const SAY_OK : str = "accepted"; +pub const SAY_BAD : str = "refused"; +pub const SAY_LANDS : str = "the shape lands"; +pub const SAY_STOPS : str = "the shape does not land"; +pub const SAY_COUNT : str = "{0} of {1} paths refused"; +pub const SAY_NOTE : str = "A file that exists is always fine: finding the file a title describes is the point. A new file needs a folder that exists, a citation in the issue, and a language the only-t27 rule allows. More than 12 paths is not a task but a project."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/shaping.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_paths_four_facts { + assert K_PATHS == 4; + assert K_FACT_BITS[0] == 1; + assert K_FACT_BITS[3] == 8; +} + +test the_start_has_one_path_of_each_kind { + assert K_PRESET_P0[0] == 1; + assert K_PRESET_P1[0] == 6; + assert K_PRESET_P3[0] == 14; + assert K_PATHS <= K_MAX_BOUNDARY_PATHS; +} diff --git a/apps/website/public/widgets/hive-boundary/index.html b/apps/website/public/widgets/hive-boundary/index.html new file mode 100644 index 0000000000..796ccefbdc --- /dev/null +++ b/apps/website/public/widgets/hive-boundary/index.html @@ -0,0 +1,47 @@ + + + + + + +A boundary: which files a task may reserve · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

A boundary: which files a task may reserve

+

Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is path_accepted and shape_lands, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 2 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-boundary/logic.js b/apps/website/public/widgets/hive-boundary/logic.js new file mode 100644 index 0000000000..7be9bc1a59 --- /dev/null +++ b/apps/website/public/widgets/hive-boundary/logic.js @@ -0,0 +1,36 @@ +// Generated by scripts/t27-logic.mjs from shaping.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: e887aacb9648b6efe25fd8fdd334e671e099692647f1d6836d121fe082205b05 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-boundary/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABFgNgAn9/AX9gA39/fwF/YAR/f39/AX8DBQQAAQICBAUBcAEBAQUDAQAQBgkBfwFBgIDAAAsHVQUGbWVtb3J5AgAPdDI3X2F0dGVtcHRfZHVlAAAPdDI3X3NoYXBlX2xhbmRzAAERdDI3X3BhdGhfYWNjZXB0ZWQAAhB0Mjdfc2hhcGVfYWN0aW9uAAMKZAQMACAARSABQecCS3ILEQAgACACckUgAUF/akEMSXELIQEBf0EBIQQCQCAAQQFxDQAgASADQQFzcSACcSEECyAECyEAQQBBAUEAQQIgA0EBcRsgAkECSRsgAUEBcRtBACAAGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function shapeAction(missing, container, attempts, told) { + return W.t27_shape_action(int(missing, 0, 255, "shape_action.missing"), container ? 1 : 0, int(attempts, 0, 4294967295, "shape_action.attempts"), told ? 1 : 0) & 255; +} +function pathAccepted(exists, dirExists, cited, foreignNew) { + return (W.t27_path_accepted(exists ? 1 : 0, dirExists ? 1 : 0, cited ? 1 : 0, foreignNew ? 1 : 0) & 1) !== 0; +} +function shapeLands(missingAfter, paths, rejected) { + return (W.t27_shape_lands(int(missingAfter, 0, 255, "shape_lands.missing_after"), int(paths, 0, 4294967295, "shape_lands.paths"), int(rejected, 0, 4294967295, "shape_lands.rejected")) & 1) !== 0; +} +function attemptDue(attempts, minutesSinceLast) { + return (W.t27_attempt_due(int(attempts, 0, 4294967295, "attempt_due.attempts"), int(minutesSinceLast, 0, 4294967295, "attempt_due.minutes_since_last")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + attemptDue, + pathAccepted, + shapeAction, + shapeLands +}; diff --git a/apps/website/public/widgets/hive-boundary/logic.t27 b/apps/website/public/widgets/hive-boundary/logic.t27 new file mode 100644 index 0000000000..e2bf702451 --- /dev/null +++ b/apps/website/public/widgets/hive-boundary/logic.t27 @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks +; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix +; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete. +; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most +; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as +; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703, +; #7642), which the rule counts as missing. +; -- +; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section). +; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a +; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either +; SHAPED by her (the missing sections are written by a model, every boundary path is checked against +; the repository, and the body is written back only if judge() then returns 0), or, when shaping +; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing. +; -- +; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the +; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is +; accepted only when the issue itself cites it and its directory exists, and never when it would be a +; new file in a language the only-t27 rule denies. +; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenShaping; + +/// What the Queen does with one open issue. +/// LEAVE nothing: it is ready, a container, or already told +/// SHAPE ask for the missing sections, judge them, write them back if they pass +/// TELL comment once, naming what is missing +pub const SH_LEAVE : u8 = 0; +pub const SH_SHAPE : u8 = 1; +pub const SH_TELL : u8 = 2; + +/// Attempts at shaping one issue before its author is told instead. +pub const SHAPE_ATTEMPT_LIMIT : u32 = 2; +/// Issues shaped per round. A shape is one model call; the round has other work. +pub const SHAPES_PER_ROUND : u32 = 3; +/// Minutes between two attempts on one issue, so a refused shape is not retried every round. +pub const SHAPE_RETRY_MINUTES : u32 = 360; +/// A boundary is a reservation; one naming more files than this is not a task, it is a project. +pub const MAX_BOUNDARY_PATHS : u32 = 12; + +/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap; +/// `attempts`: shapes tried so far; `told`: the author was already told. +pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 { + if (missing == 0) { return SH_LEAVE; } + if (container) { return SH_LEAVE; } + if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; } + if (told) { return SH_LEAVE; } + return SH_TELL; +} + +/// Is one proposed boundary path acceptable? +/// exists the file is in the repository at the base ref +/// dir_exists its directory is (a new file beside its siblings) +/// cited the issue's own title or body names this path +/// foreign_new it would be a NEW file whose extension the only-t27 rule denies to new work +/// A file that exists is always acceptable: finding the file a title describes is the point. A file +/// that does not exist must be cited by the issue, in a directory that exists, and not foreign. +pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool { + if (exists) { return true; } + if (dir_exists == false) { return false; } + if (foreign_new) { return false; } + return cited; +} + +/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and +/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted. +pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool { + if (missing_after != 0) { return false; } + if (paths == 0) { return false; } + if (paths > MAX_BOUNDARY_PATHS) { return false; } + return rejected == 0; +} + +/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue. +pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool { + if (attempts == 0) { return true; } + return minutes_since_last >= SHAPE_RETRY_MINUTES; +} + +test a_ready_issue_and_a_container_are_left_alone { + assert shape_action(0, false, 0, false) == SH_LEAVE; + assert shape_action(15, true, 0, false) == SH_LEAVE; + assert shape_action(1, true, 9, false) == SH_LEAVE; +} + +test an_unready_issue_is_shaped_then_its_author_is_told_once { + assert shape_action(1, false, 0, false) == SH_SHAPE; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE; + assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE; +} + +test a_boundary_path_must_be_true_of_the_repository { + // an existing file, cited or found + assert path_accepted(true, true, false, false) == true; + assert path_accepted(true, true, true, false) == true; + // a new spec the issue asks for, beside its siblings + assert path_accepted(false, true, true, false) == true; + // a new file the issue never mentioned: invented + assert path_accepted(false, true, false, false) == false; + // a new file in a directory that does not exist + assert path_accepted(false, false, true, false) == false; + // a new .py or .ts the only-t27 rule denies, even when cited + assert path_accepted(false, true, true, true) == false; +} + +test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds { + assert shape_lands(0, 1, 0) == true; + assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true; + assert shape_lands(1, 3, 0) == false; + assert shape_lands(0, 0, 0) == false; + assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false; + assert shape_lands(0, 3, 1) == false; +} + +test a_refused_shape_is_not_retried_every_round { + assert attempt_due(0, 0) == true; + assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false; + assert attempt_due(1, SHAPE_RETRY_MINUTES) == true; +} + +invariant the_codes_and_limits_never_move { + assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2; + assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0; + assert MAX_BOUNDARY_PATHS > 0; +} diff --git a/apps/website/public/widgets/hive-boundary/tool.js b/apps/website/public/widgets/hive-boundary/tool.js new file mode 100644 index 0000000000..17f8e48508 --- /dev/null +++ b/apps/website/public/widgets/hive-boundary/tool.js @@ -0,0 +1,31 @@ +// hive-boundary/tool.js -- lesson 2 of queen-and-hive: which files a task may reserve. Each path is +// path_accepted of the four facts the reader taps, and the gate is shape_lands of the result, both +// from gHashTag/t27 specs/queen/shaping.t27 compiled to wasm (./logic.js). The host only counts the +// refused paths it hands to shape_lands. Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-boundary.t27. +import { pathAccepted, shapeLands } from './logic.js' +import { K, W, ask, controls, el, fill, gate, head, pill, root, say, source, taps } from '../hive-kit.js' + +const N = K('K_PATHS', 4), BITS = K('K_FACT_BITS', [1, 2, 4, 8]) +const keys = [...Array(N)].map((_, i) => `p${i}`) +const ctl = controls([ + ...keys.map((key) => ({ key, kind: 'hidden', min: 0, max: 15 })), + { key: 'left', label: say('SAY_LEFT'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { ...Object.fromEntries(keys.map((k, i) => [k, K(`K_PRESET_P${i}`, [])])), left: K('K_PRESET_LEFT', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const ok = keys.map((k) => { const m = s[k]; return pathAccepted(!!(m & BITS[0]), !!(m & BITS[1]), !!(m & BITS[2]), !!(m & BITS[3])) }) + const refused = ok.filter((v) => !v).length + const lands = shapeLands(s.left ? 1 : 0, N, refused) + out.replaceChildren( + ...keys.map((k, i) => el('div', { class: 'hk-path' }, + el('p', { class: 'hk-path-head' }, el('code', {}, say('SAY_PATHS', i)), pill(ok[i] ? say('SAY_OK') : say('SAY_BAD'), ok[i] ? 0 : 6)), + taps(BITS.map((b, j) => ({ label: say('SAY_FACTS', j), pressed: !!(s[k] & b), tone: ok[i] ? 0 : 6 })), (j) => ctl.set(k, s[k] ^ BITS[j]), say('SAY_PATHS', i)))), + head(fill(say('SAY_COUNT'), refused, N)), + gate(lands, lands ? say('SAY_LANDS') : say('SAY_STOPS'), fill(say('SAY_COUNT'), refused, N))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-burn/card.png b/apps/website/public/widgets/hive-burn/card.png new file mode 100644 index 0000000000..7b57f49774 Binary files /dev/null and b/apps/website/public/widgets/hive-burn/card.png differ diff --git a/apps/website/public/widgets/hive-burn/hive-burn.t27 b/apps/website/public/widgets/hive-burn/hive-burn.t27 new file mode 100644 index 0000000000..6fbb21cc7e --- /dev/null +++ b/apps/website/public/widgets/hive-burn/hive-burn.t27 @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-burn.t27 -- Tokens that burn: a month to spend them +; Source of truth for public/widgets/hive-burn/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-burn/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/token-wallet.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 21 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags today from day 0 to day 60. Each batch is days_left(the day it +; landed, today, 30). The list is ordered by those answers, which is the spend order the spec names, +; soonest burn first. + +; WHAT IT DOES NOT CLAIM: the spec marks the burn as decided but not yet in the ledger; the page +; shows the rule, not a balance. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_burn; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-burn"; +pub const TITLE : str = "Tokens that burn: a month to spend them"; +pub const DESCRIPTION : str = "Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first."; +pub const IMAGE_ALT : str = "Burn card on day 25: three bars on a 60-day strip. The package month from day 0 has 5 days left, gold; the start gift from day 3 has 8 days; the top-up from day 12 has 17 days, green. A white line marks today."; +pub const HOOK : str = "All money goes into tokens, and the tokens burn within the month: a person who paid makes content instead of saving tokens."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is days_left, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 21 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_TODAY_MAX : u8 = 60; +pub const K_MONTH_DAYS : u16 = 30; +pub const K_BATCH_DAYS : [3]u8 = [0, 3, 12]; +pub const K_PRESET_TODAY : [4]u8 = [25, 2, 31, 45]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Day 25", "Day 2", "Day 31, the month burned", "Day 45"]; +pub const SAY_ASK : str = "How long do tokens last, and which ones are spent first?"; +pub const SAY_TODAY : str = "today, day"; +pub const SAY_BATCHES : [3]str = ["the package month", "the start gift", "a top-up"]; +pub const SAY_LANDED : str = "landed on day {0}"; +pub const SAY_LEFT : str = "{0} days left"; +pub const SAY_GONE : str = "burned"; +pub const SAY_STRIP : str = "days 0 to 60, one bar a batch"; +pub const SAY_ORDER : str = "spent first: the soonest to burn"; +pub const SAY_NOTE : str = "The spec warns at 7, 3 and 1 days. Tokens held before the burn existed never burn. Whether bought top-ups should live longer is still the owner's call: one line, TOPUP_DAYS."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/token-wallet.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test a_month_is_thirty_days { + assert K_MONTH_DAYS == 30; + assert K_TODAY_MAX == K_MONTH_DAYS * 2; +} + +test the_batches_land_in_the_first_month { + assert K_BATCH_DAYS[2] < K_MONTH_DAYS; + assert K_PRESET_TODAY[0] == 25; +} diff --git a/apps/website/public/widgets/hive-burn/index.html b/apps/website/public/widgets/hive-burn/index.html new file mode 100644 index 0000000000..bbe2c20562 --- /dev/null +++ b/apps/website/public/widgets/hive-burn/index.html @@ -0,0 +1,47 @@ + + + + + + +Tokens that burn: a month to spend them · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Tokens that burn: a month to spend them

+

Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is days_left, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 21 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-burn/logic.js b/apps/website/public/widgets/hive-burn/logic.js new file mode 100644 index 0000000000..7e64e84997 --- /dev/null +++ b/apps/website/public/widgets/hive-burn/logic.js @@ -0,0 +1,40 @@ +// Generated by scripts/t27-logic.mjs from token-wallet.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 425a06b53a9250178536c29d0dac0081f36be8f05199bf622f747f96e040fc11 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-burn/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABDQJgA39/fwF/YAF/AX8DBgUAAQEBAQQFAXABAQEFAwEAEAYJAX8BQYCAwAALB2IGBm1lbW9yeQIADXQyN19kYXlzX2xlZnQAABB0MjdfcG9ydHJhaXRzX2luAAEMdDI3X3JlZWxzX2luAAIQdDI3X3ByaWNlX3Rva2VucwADEHQyN19tb250aF90b2tlbnMABApHBRsAQQAgAiAAakH//wNxIgAgAWsiAiACIABLGwsHACAAQQxuCwgAIABBjgJuCw8AIABBAXRBhydqQYgnbgsIACAAQcgBbAs="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function monthTokens(usd) { + return W.t27_month_tokens(int(usd, 0, 4294967295, "month_tokens.usd")) >>> 0; +} +function priceTokens(costMicroUsd) { + return W.t27_price_tokens(int(costMicroUsd, 0, 4294967295, "price_tokens.cost_micro_usd")) >>> 0; +} +function reelsIn(tokens) { + return W.t27_reels_in(int(tokens, 0, 4294967295, "reels_in.tokens")) >>> 0; +} +function portraitsIn(tokens) { + return W.t27_portraits_in(int(tokens, 0, 4294967295, "portraits_in.tokens")) >>> 0; +} +function daysLeft(grantedDay, today, life) { + return W.t27_days_left(int(grantedDay, 0, 65535, "days_left.granted_day"), int(today, 0, 65535, "days_left.today"), int(life, 0, 65535, "days_left.life")) & 65535; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + daysLeft, + monthTokens, + portraitsIn, + priceTokens, + reelsIn +}; diff --git a/apps/website/public/widgets/hive-burn/logic.t27 b/apps/website/public/widgets/hive-burn/logic.t27 new file mode 100644 index 0000000000..9b8ec63538 --- /dev/null +++ b/apps/website/public/widgets/hive-burn/logic.t27 @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a +// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and +// tokens that are not spent within a month burn. +// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts +// (the month a package buys, what it is worth in reels and portraits, when it burns), +// read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx, +// player/src/components/Agents/PackageCards.tsx) and by render's billing +// (render/src/agent/billing-shared.ts: the rate and the markup every price is made with). +// Generated: `t27c gen-ts` of this file is vendored into the host as +// apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports +// the numbers from there and never restates them; a host test runs this file's vectors +// against the host. +// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent +// within the month or they burn -- so a person who paid makes content instead of saving +// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the +// credits that burn soonest first, and so does this. +// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts +// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN). +// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call. +// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host +// already charges. The month, the burn and the spend order are DECIDED but not yet in the +// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an +// expiry are the next change, and the burn must not be sold before it lands. +// Owner decision still open: whether tokens bought on top of a package burn in 30 days like +// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's +// bought credits do (TOPUP_DAYS is the one line to change). +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::token_wallet { + + pub const KIND : str = "automation"; + pub const ID : str = "token-wallet"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 1; + + // ---- One rate, one markup ----------------------------------------------------------- + pub const TOKEN_MICRO_USD : u32 = 5000; // one token is half a cent, everywhere + pub const MARKUP_PCT : u32 = 200; // an operation is sold at twice what it costs us + pub const MICRO_PER_USD : u32 = 1000000; + + // ---- What a month buys ---------------------------------------------------------------- + // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is + // kept back, the margin is the markup on what is spent and whatever burns. + pub const SETUP_INCLUDES_MONTH : bool = true; // the launch price carries the first month + + // ---- The burn ----------------------------------------------------------------------- + pub const MONTH_DAYS : u16 = 30; // a package month burns 30 days after it lands + pub const TOPUP_DAYS : u16 = 30; // owner's rule: bought tokens burn the same way + pub const START_DAYS : u16 = 30; // the start gift burns too + pub const SPEND_ORDER : str = "soonest-burn-first"; + pub const BURNS_BACKDATED : bool = false; // tokens held before v1 never burn + pub const WARN_DAYS_FIRST : u16 = 7; + pub const WARN_DAYS_SECOND : u16 = 3; + pub const WARN_DAYS_LAST : u16 = 1; + + // ---- The start gift ------------------------------------------------------------------- + pub const START_TOKENS : u32 = 20; // granted once, on the first paid call + + // ---- What tokens are worth, in things a person can picture ---------------------------- + // The reference reel is the story reel's own estimate (crm-story-reel.t27 + // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the + // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds + // both to those specs, so the welcome screen cannot promise a reel at an old price. + pub const REEL_TOKENS : u32 = 270; + pub const PORTRAIT_TOKENS : u32 = 12; + + // Dollars a month to tokens a month. + fn month_tokens(usd: u32) -> u32 { + return usd * MICRO_PER_USD / TOKEN_MICRO_USD; + } + + // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so + // nothing is sold below twice its cost. + fn price_tokens(cost_micro_usd: u32) -> u32 { + return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD; + } + + // Whole reels a number of tokens buys. + fn reels_in(tokens: u32) -> u32 { + return tokens / REEL_TOKENS; + } + + // Whole portraits a number of tokens buys. + fn portraits_in(tokens: u32) -> u32 { + return tokens / PORTRAIT_TOKENS; + } + + // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone). + fn days_left(granted_day: u16, today: u16, life: u16) -> u16 { + if (today >= granted_day + life) { + return 0; + } + return granted_day + life - today; + } + + test "every dollar of a package month becomes tokens at the one rate" { + assert(month_tokens(15) == 3000); + assert(month_tokens(35) == 7000); + assert(month_tokens(50) == 10000); + assert(SETUP_INCLUDES_MONTH); + } + + test "an operation is sold at twice its cost, rounded up" { + assert(MARKUP_PCT == 200); + assert(price_tokens(30000) == 12); + assert(price_tokens(20000) == 8); + assert(price_tokens(1) == 1); + } + + test "a month burns on day thirty and nothing held before it burns" { + assert(days_left(0, 0, MONTH_DAYS) == 30); + assert(days_left(0, 23, MONTH_DAYS) == 7); + assert(days_left(0, 30, MONTH_DAYS) == 0); + assert(TOPUP_DAYS == MONTH_DAYS); + assert(SPEND_ORDER == "soonest-burn-first"); + assert(!BURNS_BACKDATED); + } + + test "the warnings come at seven, three and one day left" { + assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND); + assert(WARN_DAYS_SECOND > WARN_DAYS_LAST); + assert(WARN_DAYS_FIRST < MONTH_DAYS); + } + + test "the start gift buys one portrait and no reel" { + assert(portraits_in(START_TOKENS) == 1); + assert(reels_in(START_TOKENS) == 0); + } + + test "a Blogger month is twenty-five reels" { + assert(reels_in(month_tokens(35)) == 25); + assert(reels_in(month_tokens(15)) == 11); + assert(reels_in(month_tokens(50)) == 37); + assert(portraits_in(month_tokens(35)) == 583); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-burn/tool.js b/apps/website/public/widgets/hive-burn/tool.js new file mode 100644 index 0000000000..8bd54ca014 --- /dev/null +++ b/apps/website/public/widgets/hive-burn/tool.js @@ -0,0 +1,31 @@ +// hive-burn/tool.js -- lesson 21 of queen-and-hive: tokens that burn. Each batch's countdown is +// days_left(landed, today, MONTH_DAYS), the fn body of gHashTag/t27 specs/automation/token-wallet.t27 +// compiled to wasm (./logic.js); the list is ordered by those answers. Words are SAY_ and numbers K_ +// constants of specs/widgets/hive-burn.t27. +import { daysLeft } from './logic.js' +import { COLORS, K, W, ask, canvas, controls, el, fill, head, ladder, root, say, source, svgEl } from '../hive-kit.js' + +const TMAX = K('K_TODAY_MAX', 60), LIFE = K('K_MONTH_DAYS', 30), LANDED = K('K_BATCH_DAYS', []) +const ctl = controls([{ key: 'today', label: say('SAY_TODAY'), min: 0, max: TMAX, step: 1 }], { names: W.SAY_PRESET_NAMES, values: { today: K('K_PRESET_TODAY', []) } }, draw) +const svg = canvas('hk-strip', say('SAY_STRIP')) +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, svg, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const TONES = [1, 3, 0] + +function draw(s) { + const left = LANDED.map((d) => daysLeft(d, s.today, LIFE)) + const w = 420, x = (d) => 20 + (d / TMAX) * (w - 40) + svg.replaceChildren() + svg.setAttribute('viewBox', `0 0 ${w} 190`) + LANDED.forEach((d, i) => { + const y = 20 + i * 50 + svg.append(svgEl('rect', { x: x(d), y, width: x(d + LIFE) - x(d), height: 30, rx: 8, fill: COLORS[TONES[i]], opacity: left[i] ? 0.85 : 0.18 })) + svg.append(svgEl('text', { x: x(d) + 6, y: y + 20, class: 'dk-txt', style: 'fill:#020403;font-weight:700;font-size:13px' }, say('SAY_BATCHES', i))) + }) + svg.append(svgEl('line', { x1: x(s.today), x2: x(s.today), y1: 10, y2: 170, stroke: '#fff', 'stroke-width': 3 })) + for (const t of [0, 15, 30, 45, 60].filter((t) => t <= TMAX)) svg.append(svgEl('text', { x: x(t), y: 186, class: 'dk-tick', 'text-anchor': 'middle' }, t)) + const order = LANDED.map((_, i) => i).sort((a, b) => (left[a] || Infinity) - (left[b] || Infinity)) + out.replaceChildren(head(say('SAY_ORDER')), ladder(order.map((i) => ({ label: say('SAY_BATCHES', i), sub: `${fill(say('SAY_LANDED'), LANDED[i])} · ${left[i] ? fill(say('SAY_LEFT'), left[i]) : say('SAY_GONE')}`, tone: TONES[i], on: left[i] > 0 })))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-called/card.png b/apps/website/public/widgets/hive-called/card.png new file mode 100644 index 0000000000..ae5d8d82ea Binary files /dev/null and b/apps/website/public/widgets/hive-called/card.png differ diff --git a/apps/website/public/widgets/hive-called/hive-called.t27 b/apps/website/public/widgets/hive-called/hive-called.t27 new file mode 100644 index 0000000000..65f982237e --- /dev/null +++ b/apps/website/public/widgets/hive-called/hive-called.t27 @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-called.t27 -- Who is called: an agent bot in a group of agents +; Source of truth for public/widgets/hive-called/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-called/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 22 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader toggles tagged, the all tag, a reply to this agent, and the agent's own +; topic. The two cards are addressed(flags, from a person) and addressed(flags, from a bot); every +; table cell is addressed of its row's single flag and its column's sender. + +; WHAT IT DOES NOT CLAIM: it sends nothing to Telegram; a live group also needs Bot-to-Bot +; Communication Mode on in @BotFather. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_called; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-called"; +pub const TITLE : str = "Who is called: an agent bot in a group of agents"; +pub const DESCRIPTION : str = "Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot."; +pub const IMAGE_ALT : str = "Called card: a message written in the agent's own topic, with no tag and no reply. The person's message wakes the agent, green; the same message from another agent's bot does not, grey. A table of five ways to call reads tag, reply, all tag, own topic, nothing for both senders."; +pub const HOOK : str = "A bot never triggers the whole hive and never wakes a topic's owner by merely writing there. Only a tag or a reply calls an agent from another bot."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is addressed, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 22 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_FLAGS : u8 = 4; +pub const K_PRESET_TAGGED : [4]u8 = [0, 1, 0, 0]; +pub const K_PRESET_ALL : [4]u8 = [0, 0, 1, 0]; +pub const K_PRESET_REPLY : [4]u8 = [0, 0, 0, 1]; +pub const K_PRESET_TOPIC : [4]u8 = [1, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["In the agent's topic", "Tagged by name", "The all tag", "A reply to the agent"]; +pub const SAY_ASK : str = "A message lands in the hive group. Which agent must answer it?"; +pub const SAY_FLAGS : [4]str = ["tagged by name", "the @all tag", "a reply to this agent", "in this agent's topic"]; +pub const SAY_SENDERS : [2]str = ["a person writes", "another agent's bot writes"]; +pub const SAY_CALLED : str = "called: answers"; +pub const SAY_QUIET : str = "not called: stays quiet"; +pub const SAY_TABLE : str = "one way at a time"; +pub const SAY_NOTHING : str = "none of them"; +pub const SAY_YES : str = "calls"; +pub const SAY_NO : str = "quiet"; +pub const SAY_NOTE : str = "Telegram gives no loop protection between bots. So the rule is narrow for bots: a tag or a reply, nothing else, and the depth and the cooldown of the next two lessons stop the rest."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_ways_to_call { + assert K_FLAGS == 4; + assert K_PRESET_TOPIC[0] == 1; +} + +test each_preset_sets_one_flag { + assert K_PRESET_TAGGED[1] + K_PRESET_ALL[1] + K_PRESET_REPLY[1] + K_PRESET_TOPIC[1] == 1; + assert K_PRESET_TAGGED[0] + K_PRESET_ALL[0] + K_PRESET_REPLY[0] + K_PRESET_TOPIC[0] == 1; +} diff --git a/apps/website/public/widgets/hive-called/index.html b/apps/website/public/widgets/hive-called/index.html new file mode 100644 index 0000000000..85af67e4f2 --- /dev/null +++ b/apps/website/public/widgets/hive-called/index.html @@ -0,0 +1,47 @@ + + + + + + +Who is called: an agent bot in a group of agents · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Who is called: an agent bot in a group of agents

+

Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is addressed, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 22 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-called/logic.js b/apps/website/public/widgets/hive-called/logic.js new file mode 100644 index 0000000000..76652519b2 --- /dev/null +++ b/apps/website/public/widgets/hive-called/logic.js @@ -0,0 +1,116 @@ +// Generated by scripts/t27-logic.mjs from agent-hive-group.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 9cdd5ed3872b60e900a364d843f908920334408f965bde194d233b64534076f9 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-called/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJAVgAX8Bf2ACf38Bf2ADf39/AX9gBH9/f38Bf2AFf39/f38BfwMZGAABAAACAAIBAAEAAQEAAQADAAAAAgAABAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB74DGQZtZW1vcnkCABR0MjdfZG1faGFuZG9mZnNfc2VudAAADXQyN19kbV90YWdfb2sAAQ50MjdfZG1fY2FycmllcwACEHQyN19kbV9ydW5zX3R1cm4AAwx0MjdfZG1faGVhcmQABBF0Mjdfc3VtbWFyeV9jb3VudAAFEnQyN19yZWNyZWF0ZV90b3BpYwAGEXQyN19tYXlfc2V0X2JyaWVmAAcOdDI3X2JyaWVmX2ZpdHMACBB0MjdfYnJpZWZfc291cmNlAAkTdDI3X2tlZXBzX3NlZW5fbWFyawAKC3QyN190dXJuX29rAAsVdDI3X2hlYXJkX3doaWxlX211dGVkAAwJdDI3X211dGVkAA0ZdDI3X21heV9hbnN3ZXJfYWZ0ZXJfc3RvcAAOC3QyN19zdG9wcGVkAA8OdDI3X2ZvbGxvd3NfdXAAEBF0Mjdfc3BlYWtzX29uX2FsbAARDnQyN190eXBpbmdfZHVlABIPdDI3X2NsYWltX2ZyZXNoABMOdDI3X21heV9hbnN3ZXIAFAp0MjdfY29vbGVkABUOdDI3X2NoYWluX29wZW4AFg10MjdfYWRkcmVzc2VkABcK7AIYDAAgAEECIABBAkkbCyoBAX9BASECAkACQAJAIABBf2oOAgIAAQsgAUHA7u0ASQ8LQQAhAgsgAgsHACAAQQJGCwcAIABBAUYLDQAgAEEBcyABIAJxcgsTACAAQcgBIABByAFJG0EyIAAbCxIAIAAgAXFBf3MgAkG/zyRLcQsKACAAIAFBAXNxCwgAIABB6QdJCw8AQQIgAUEBcSAAQQFxGwsEACAACwQAIAALDQAgACABQf/c2wFLcgsKACAAQYDd2wFJCw8AIABBAXMgAUG/zyRLcgsJACAAQcDPJEkLFQAgAEF/cyABcSACcSADQcCpB0lxCwUAIABFCwgAIABBnx9LCwkAIABBoPc2SQsjAQF/QQEhAwJAIABBAXFFDQAgAUEDSSACQYcnS3EhAwsgAwsIACAAQYcnSwsHACAAQQNJCygBAX9BASEFAkAgAEEBcQ0AIAJBAXENACAEQQFzIAEgA3JxIQULIAUL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function addressed(tagged, allTag, replyToMe, ownTopic, fromBot) { + return (W.t27_addressed(tagged ? 1 : 0, allTag ? 1 : 0, replyToMe ? 1 : 0, ownTopic ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function chainOpen(depth) { + return (W.t27_chain_open(int(depth, 0, 4294967295, "chain_open.depth")) & 1) !== 0; +} +function cooled(sinceLastMs) { + return (W.t27_cooled(int(sinceLastMs, 0, 4294967295, "cooled.since_last_ms")) & 1) !== 0; +} +function mayAnswer(fromBot, depth, sinceLastMs) { + return (W.t27_may_answer(fromBot ? 1 : 0, int(depth, 0, 4294967295, "may_answer.depth"), int(sinceLastMs, 0, 4294967295, "may_answer.since_last_ms")) & 1) !== 0; +} +function claimFresh(ageMs) { + return (W.t27_claim_fresh(int(ageMs, 0, 4294967295, "claim_fresh.age_ms")) & 1) !== 0; +} +function typingDue(sinceLastMs) { + return (W.t27_typing_due(int(sinceLastMs, 0, 4294967295, "typing_due.since_last_ms")) & 1) !== 0; +} +function speaksOnAll(rank) { + return (W.t27_speaks_on_all(int(rank, 0, 4294967295, "speaks_on_all.rank")) & 1) !== 0; +} +function followsUp(fromBot, samePerson, sameThread, ageMs) { + return (W.t27_follows_up(fromBot ? 1 : 0, samePerson ? 1 : 0, sameThread ? 1 : 0, int(ageMs, 0, 4294967295, "follows_up.age_ms")) & 1) !== 0; +} +function stopped(sinceStopMs) { + return (W.t27_stopped(int(sinceStopMs, 0, 4294967295, "stopped.since_stop_ms")) & 1) !== 0; +} +function mayAnswerAfterStop(fromBot, sinceStopMs) { + return (W.t27_may_answer_after_stop(fromBot ? 1 : 0, int(sinceStopMs, 0, 4294967295, "may_answer_after_stop.since_stop_ms")) & 1) !== 0; +} +function muted(sinceMuteMs) { + return (W.t27_muted(int(sinceMuteMs, 0, 4294967295, "muted.since_mute_ms")) & 1) !== 0; +} +function heardWhileMuted(tagged, sinceMuteMs) { + return (W.t27_heard_while_muted(tagged ? 1 : 0, int(sinceMuteMs, 0, 4294967295, "heard_while_muted.since_mute_ms")) & 1) !== 0; +} +function turnOk(answerSent, reactionOk) { + return (W.t27_turn_ok(answerSent ? 1 : 0, reactionOk ? 1 : 0) & 1) !== 0; +} +function keepsSeenMark(answerSent) { + return (W.t27_keeps_seen_mark(answerSent ? 1 : 0) & 1) !== 0; +} +function briefSource(hasTopicBrief, hasGroupBrief) { + return W.t27_brief_source(hasTopicBrief ? 1 : 0, hasGroupBrief ? 1 : 0) & 255; +} +function briefFits(chars) { + return (W.t27_brief_fits(int(chars, 0, 4294967295, "brief_fits.chars")) & 1) !== 0; +} +function maySetBrief(isAdmin, fromBot) { + return (W.t27_may_set_brief(isAdmin ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function recreateTopic(stored, threadFound, sinceLastTryMs) { + return (W.t27_recreate_topic(stored ? 1 : 0, threadFound ? 1 : 0, int(sinceLastTryMs, 0, 4294967295, "recreate_topic.since_last_try_ms")) & 1) !== 0; +} +function summaryCount(asked) { + return W.t27_summary_count(int(asked, 0, 4294967295, "summary_count.asked")) >>> 0; +} +function dmHeard(fromBot, colleague, tagOk) { + return (W.t27_dm_heard(fromBot ? 1 : 0, colleague ? 1 : 0, tagOk ? 1 : 0) & 1) !== 0; +} +function dmRunsTurn(kind) { + return (W.t27_dm_runs_turn(int(kind, 0, 255, "dm_runs_turn.kind")) & 1) !== 0; +} +function dmCarries(kind) { + return (W.t27_dm_carries(int(kind, 0, 255, "dm_carries.kind")) & 1) !== 0; +} +function dmTagOk(kind, sinceAskMs) { + return (W.t27_dm_tag_ok(int(kind, 0, 255, "dm_tag_ok.kind"), int(sinceAskMs, 0, 4294967295, "dm_tag_ok.since_ask_ms")) & 1) !== 0; +} +function dmHandoffsSent(asked) { + return W.t27_dm_handoffs_sent(int(asked, 0, 4294967295, "dm_handoffs_sent.asked")) >>> 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + addressed, + briefFits, + briefSource, + chainOpen, + claimFresh, + cooled, + dmCarries, + dmHandoffsSent, + dmHeard, + dmRunsTurn, + dmTagOk, + followsUp, + heardWhileMuted, + keepsSeenMark, + mayAnswer, + mayAnswerAfterStop, + maySetBrief, + muted, + recreateTopic, + speaksOnAll, + stopped, + summaryCount, + turnOk, + typingDue +}; diff --git a/apps/website/public/widgets/hive-called/logic.t27 b/apps/website/public/widgets/hive-called/logic.t27 new file mode 100644 index 0000000000..2f801b0db7 --- /dev/null +++ b/apps/website/public/widgets/hive-called/logic.t27 @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group. +// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots). +// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group". +// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/. +// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode +// switched on in @BotFather for every bot, which no API call can do. +// Telegram gives no loop protection between bots, so the depth and the cooldown +// below are the only thing that stops two agents from answering each other forever. +// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window. +// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept +// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute. +// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs, +// a topic a person deleted, and /summary. +// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private +// message to another bot by @username when Bot-to-Bot Communication Mode is on for both +// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the +// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn, +// and only while its ask is fresh. +// Earlier laws are unchanged; each version only adds, so a host on an older one stays true. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::agent_hive_group { + pub const KIND : str = "automation"; + pub const ID : str = "agent-hive-group"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 4; + + // Bot messages in a row after one human message: human -> A -> B -> C, then silence. + pub const MAX_BOT_DEPTH : u32 = 3; + // One agent answers another agent at most this often. + pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000; + // How long "create the bot for agent X" waits for Telegram's creation message. + pub const CLAIM_TTL_MS : u32 = 900000; + // The tag that calls every agent of the hive at once. + pub const ALL_TAG : str = "@all"; + // Suggested username of an agent's bot: t27__bot. + pub const BOT_USERNAME_PREFIX : str = "t27_"; + pub const BOT_USERNAME_SUFFIX : str = "_bot"; + + // A person may call an agent by its tag, by the all tag, by replying to it, + // or by writing in the agent's own topic. Another bot only by tag or reply: + // a bot never triggers the whole hive, and never wakes a topic's owner by + // merely writing there. + fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool { + return tagged || reply_to_me || (!from_bot && (all_tag || own_topic)); + } + + // depth = how many bot messages already stand between this one and the person. + fn chain_open(depth: u32) -> bool { + return depth < MAX_BOT_DEPTH; + } + + fn cooled(since_last_ms: u32) -> bool { + return since_last_ms >= BOT_REPLY_COOLDOWN_MS; + } + + // A person is always answered; a bot only inside the chain and after the cooldown. + fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool { + return !from_bot || (chain_open(depth) && cooled(since_last_ms)); + } + + fn claim_fresh(age_ms: u32) -> bool { + return age_ms < CLAIM_TTL_MS; + } + + // ---- v2 ---- + + // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute. + // The action is sent again this long after the last one, until the answer goes out. + pub const TYPING_REFRESH_MS : u32 = 4000; + // On the all tag only this many agents answer at once, best fit first; the rest + // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team. + pub const ALL_FIRST_SPEAKERS : u32 = 1; + // A person's next message in the same topic goes to the agent that answered + // last, without a new tag, for this long. + pub const FOLLOW_UP_WINDOW_MS : u32 = 120000; + // /stop: no bot answers a bot in that chat for this long. People are still answered. + pub const STOP_WINDOW_MS : u32 = 600000; + // /mute: the agent answers in that topic only when tagged by name, for this long. + pub const MUTE_MS : u32 = 3600000; + + fn typing_due(since_last_ms: u32) -> bool { + return since_last_ms >= TYPING_REFRESH_MS; + } + + // rank = the agent's place in the best-fit order for this message, from 0. + fn speaks_on_all(rank: u32) -> bool { + return rank < ALL_FIRST_SPEAKERS; + } + + // A bot never follows up: it must tag or reply, as v1 says. + fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool { + return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS; + } + + fn stopped(since_stop_ms: u32) -> bool { + return since_stop_ms < STOP_WINDOW_MS; + } + + // A stop cuts bot-to-bot turns only; a person is always heard. + fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool { + return !from_bot || !stopped(since_stop_ms); + } + + fn muted(since_mute_ms: u32) -> bool { + return since_mute_ms < MUTE_MS; + } + + // A muted agent still answers its own tag: muting a topic must not lose a direct call. + fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool { + return tagged || !muted(since_mute_ms); + } + + // ---- v3 ---- + + // A reaction is a courtesy: chats may switch reactions off, so the answer counts + // whether or not the reaction went through. The "seen" mark stays only when the + // turn ended in an answer; a failed turn takes it away so nobody waits for nothing. + fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool { + return answer_sent; + } + + fn keeps_seen_mark(answer_sent: bool) -> bool { + return answer_sent; + } + + // /brief: standing instructions for a group or one topic, set by an admin only. + pub const MAX_BRIEF_CHARS : u32 = 1000; + // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's. + // The narrower one wins; the two are never glued together. + fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 { + if has_topic_brief { return 2; } + if has_group_brief { return 1; } + return 0; + } + + // An empty /brief is accepted: it clears the brief, it is not refused. + fn brief_fits(chars: u32) -> bool { + return chars <= MAX_BRIEF_CHARS; + } + + fn may_set_brief(is_admin: bool, from_bot: bool) -> bool { + return is_admin && !from_bot; + } + + // A person deleted an agent's topic: the stored id is stale, so it is recreated. + // Without rights the recreate fails every time, so it is tried at most this often. + pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000; + fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool { + return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS; + } + + // /summary: the topic keeps a bounded log; a bare /summary reads the default count. + pub const SUMMARY_LOG_SIZE : u32 = 200; + pub const SUMMARY_DEFAULT : u32 = 50; + fn summary_count(asked: u32) -> u32 { + if asked == 0 { return SUMMARY_DEFAULT; } + if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; } + return asked; + } + + // ---- v4: hand-off in private, bot to bot ---- + + // Every private hand-off between two agents starts with this tag, then its kind and the + // person it is for. Only a hive agent of the same owner can send it, so the person named in + // it is trusted: the turn runs in that person's name, never the owner's. + pub const DM_TAG : str = "#a2a"; + pub const DM_ASK : u8 = 1; + pub const DM_ANSWER : u8 = 2; + // An answer to a person hands off to at most this many colleagues. + pub const MAX_DM_HANDOFFS : u32 = 2; + + // In private a person is always heard; a bot only when it is a colleague (an agent bot of + // the same owner) and its message carries the hand-off tag with a known kind. + fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool { + return !from_bot || (colleague && tag_ok); + } + + // An ask runs the colleague's turn; an answer is only carried to the person. So a private + // chain is two messages long and cannot loop, whatever the agents write. + fn dm_runs_turn(kind: u8) -> bool { + return kind == DM_ASK; + } + fn dm_carries(kind: u8) -> bool { + return kind == DM_ANSWER; + } + + // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never + // writes to a person nobody asked it about. Half an hour covers the longest turn. + pub const DM_ANSWER_WINDOW_MS : u32 = 1800000; + fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool { + if kind == DM_ASK { return true; } + if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; } + return false; + } + + // How many of the hand-offs an answer asked for are sent. + fn dm_handoffs_sent(asked: u32) -> u32 { + if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; } + return asked; + } + + test "a person calls an agent by tag, all tag, reply or its own topic" { + assert(addressed(true, false, false, false, false)); + assert(addressed(false, true, false, false, false)); + assert(addressed(false, false, true, false, false)); + assert(addressed(false, false, false, true, false)); + assert(!addressed(false, false, false, false, false)); + } + + test "a bot calls an agent only by tag or reply" { + assert(addressed(true, false, false, false, true)); + assert(addressed(false, false, true, false, true)); + assert(!addressed(false, true, false, false, true)); + assert(!addressed(false, false, false, true, true)); + assert(!addressed(false, true, false, true, true)); + } + + test "the bot chain closes after three bot messages" { + assert(chain_open(0)); + assert(chain_open(2)); + assert(!chain_open(3)); + assert(!chain_open(4)); + } + + test "a bot waits out the cooldown before answering a bot" { + assert(!cooled(0)); + assert(!cooled(4999)); + assert(cooled(5000)); + } + + test "a person is always answered and a bot only inside the limits" { + assert(may_answer(false, 9, 0)); + assert(may_answer(true, 2, 5000)); + assert(!may_answer(true, 3, 5000)); + assert(!may_answer(true, 2, 4999)); + } + + test "a creation claim lives fifteen minutes" { + assert(claim_fresh(0)); + assert(claim_fresh(899999)); + assert(!claim_fresh(900000)); + } + + test "typing is sent again four seconds after the last one" { + assert(!typing_due(0)); + assert(!typing_due(3999)); + assert(typing_due(4000)); + } + + test "on the all tag only the best fit answers at once" { + assert(speaks_on_all(0)); + assert(!speaks_on_all(1)); + assert(!speaks_on_all(7)); + } + + test "a person follows up in the same topic for two minutes without a tag" { + assert(follows_up(false, true, true, 0)); + assert(follows_up(false, true, true, 119999)); + assert(!follows_up(false, true, true, 120000)); + assert(!follows_up(false, false, true, 0)); + assert(!follows_up(false, true, false, 0)); + assert(!follows_up(true, true, true, 0)); + } + + test "a stop silences bots for ten minutes and never a person" { + assert(!may_answer_after_stop(true, 0)); + assert(!may_answer_after_stop(true, 599999)); + assert(may_answer_after_stop(true, 600000)); + assert(may_answer_after_stop(false, 0)); + } + + test "a muted agent still answers its own tag for the hour" { + assert(heard_while_muted(true, 0)); + assert(!heard_while_muted(false, 0)); + assert(!heard_while_muted(false, 3599999)); + assert(heard_while_muted(false, 3600000)); + } + + test "the answer counts even when the chat refuses reactions" { + assert(turn_ok(true, false)); + assert(turn_ok(true, true)); + assert(!turn_ok(false, true)); + assert(keeps_seen_mark(true)); + assert(!keeps_seen_mark(false)); + } + + test "the topic brief wins over the group brief, and only an admin sets one" { + assert(brief_source(true, true) == 2); + assert(brief_source(true, false) == 2); + assert(brief_source(false, true) == 1); + assert(brief_source(false, false) == 0); + assert(brief_fits(0)); + assert(brief_fits(1000)); + assert(!brief_fits(1001)); + assert(may_set_brief(true, false)); + assert(!may_set_brief(false, false)); + assert(!may_set_brief(true, true)); + } + + test "a deleted topic is recreated, at most once in ten minutes" { + assert(recreate_topic(false, false, 600000)); + assert(recreate_topic(false, true, 600000)); + assert(recreate_topic(true, false, 600000)); + assert(!recreate_topic(true, true, 600000)); + assert(!recreate_topic(true, false, 599999)); + } + + test "a summary reads fifty by default and never past the log" { + assert(summary_count(0) == 50); + assert(summary_count(1) == 1); + assert(summary_count(10) == 10); + assert(summary_count(200) == 200); + assert(summary_count(201) == 200); + } + + test "in private a colleague is heard only with the hand-off tag" { + assert(dm_heard(false, false, false)); + assert(dm_heard(true, true, true)); + assert(!dm_heard(true, false, true)); + assert(!dm_heard(true, true, false)); + } + + test "an ask runs a turn, an answer is only carried" { + assert(DM_ASK == 1); + assert(DM_ANSWER == 2); + assert(dm_runs_turn(DM_ASK)); + assert(!dm_runs_turn(DM_ANSWER)); + assert(!dm_runs_turn(0)); + assert(dm_carries(DM_ANSWER)); + assert(!dm_carries(DM_ASK)); + assert(!dm_carries(0)); + } + + test "an answer is carried only while its ask is fresh" { + assert(DM_ANSWER_WINDOW_MS == 1800000); + assert(dm_tag_ok(DM_ASK, 4000000000)); + assert(dm_tag_ok(DM_ANSWER, 0)); + assert(dm_tag_ok(DM_ANSWER, 1799999)); + assert(!dm_tag_ok(DM_ANSWER, 1800000)); + assert(!dm_tag_ok(DM_ANSWER, 4000000000)); + assert(!dm_tag_ok(0, 0)); + assert(!dm_tag_ok(3, 0)); + } + + test "an answer hands off to at most two colleagues" { + assert(MAX_DM_HANDOFFS == 2); + assert(dm_handoffs_sent(0) == 0); + assert(dm_handoffs_sent(1) == 1); + assert(dm_handoffs_sent(2) == 2); + assert(dm_handoffs_sent(3) == 2); + assert(dm_handoffs_sent(100) == 2); + } + + test "VERSION says what the header's last note says" { + assert(VERSION == 4); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-called/tool.js b/apps/website/public/widgets/hive-called/tool.js new file mode 100644 index 0000000000..3049b52e41 --- /dev/null +++ b/apps/website/public/widgets/hive-called/tool.js @@ -0,0 +1,26 @@ +// hive-called/tool.js -- lesson 22 of queen-and-hive: who is called in a group of agent bots. Both +// cards and every table cell are addressed(tagged, all tag, reply, own topic, from a bot), the fn body +// of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm (./logic.js). Words are SAY_ +// and numbers K_ constants of specs/widgets/hive-called.t27. +import { addressed } from './logic.js' +import { K, W, ask, controls, el, grid, head, root, say, source, two, verdict } from '../hive-kit.js' + +const NF = K('K_FLAGS', 4) +const keys = ['tagged', 'all', 'reply', 'topic'] +const ctl = controls(keys.map((key, i) => ({ key, label: say('SAY_FLAGS', i), kind: 'toggle' })), + { names: W.SAY_PRESET_NAMES, values: { tagged: K('K_PRESET_TAGGED', []), all: K('K_PRESET_ALL', []), reply: K('K_PRESET_REPLY', []), topic: K('K_PRESET_TOPIC', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const f = keys.map((k) => !!s[k]) + const card = (bot) => { const on = addressed(f[0], f[1], f[2], f[3], bot); return verdict(on ? say('SAY_CALLED') : say('SAY_QUIET'), on ? 0 : 8, say('SAY_SENDERS', bot ? 1 : 0)) } + const one = (row) => [...Array(NF)].map((_, i) => i === row) + out.replaceChildren( + two(card(false), card(true)), + head(say('SAY_TABLE')), + grid({ rows: NF + 1, cols: 2, rowLabels: [...W.SAY_FLAGS, say('SAY_NOTHING')], colLabels: W.SAY_SENDERS, cell: (r, c) => { const v = addressed(...one(r), c === 1); return { text: v ? say('SAY_YES') : say('SAY_NO'), tone: v ? 0 : 8, on: v } } })) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-cell/card.png b/apps/website/public/widgets/hive-cell/card.png new file mode 100644 index 0000000000..71e6bef96d Binary files /dev/null and b/apps/website/public/widgets/hive-cell/card.png differ diff --git a/apps/website/public/widgets/hive-cell/hive-cell.t27 b/apps/website/public/widgets/hive-cell/hive-cell.t27 new file mode 100644 index 0000000000..2ef440ee68 --- /dev/null +++ b/apps/website/public/widgets/hive-cell/hive-cell.t27 @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-cell.t27 -- The comb answers: what an event does to a cell +; Source of truth for public/widgets/hive-cell/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-cell/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/hive.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 27 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks an event kind, whether a review accepted, the milliseconds since +; the event, and for new cells the count pending, whether cells were placed before and the seconds +; since the last batch. Each cell is cell_effect(kind, accepted) at flash_alpha(ms); the gate is +; cells_due. + +; WHAT IT DOES NOT CLAIM: it is a flat comb, not the 3D game, and reads no live events. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_cell; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-cell"; +pub const TITLE : str = "The comb answers: what an event does to a cell"; +pub const DESCRIPTION : str = "Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells."; +pub const IMAGE_ALT : str = "Comb card: ten cells, one per event kind, 400 ms after a review that accepted. The review cell flashes gold at 778 of 1000; task created is white, lease expired red, cancel grey, the six others dark. A gate below reads new cells due: no, 120 s of 300 since the last batch."; +pub const HOOK : str = "Before this spec an accepted task, a lost lease and a cancel looked the same as no news. Now each paints its cell, and the flash fades over 1800 ms."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is cell_effect, flash_alpha and cells_due, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 27 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KINDS : u8 = 10; +pub const K_MS_MAX : u16 = 2000; +pub const K_MS_STEP : u8 = 50; +pub const K_FLASH_MS : u32 = 1800; +pub const K_CELL_BATCH_SECONDS : u32 = 300; +pub const K_PENDING_MAX : u8 = 10; +pub const K_SINCE_MAX : u16 = 600; +pub const K_SINCE_STEP : u8 = 10; +pub const K_FX_TONES : [6]u8 = [9, 7, 1, 4, 6, 8]; +pub const K_PRESET_KIND : [4]u8 = [5, 5, 3, 0]; +pub const K_PRESET_ACCEPTED : [4]u8 = [1, 0, 0, 0]; +pub const K_PRESET_MS : [4]u16 = [400, 200, 900, 0]; +pub const K_PRESET_PENDING : [4]u8 = [3, 3, 1, 5]; +pub const K_PRESET_PLACED : [4]u8 = [1, 1, 1, 0]; +pub const K_PRESET_SINCE : [4]u16 = [120, 320, 30, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A review accepted", "A review sent back", "A lease expired", "The page opens"]; +pub const SAY_ASK : str = "How does the game show that something happened to a task?"; +pub const SAY_KIND : str = "event"; +pub const SAY_KIND_SHORT : [10]str = ["created", "ended", "idle", "lease", "evidence", "review", "assign", "cancel", "beat", "tick"]; +pub const SAY_KIND_NAMES : [10]str = ["task created", "task ended", "worker idle", "lease expired", "evidence", "review", "assign", "cancel", "heartbeat", "tick"]; +pub const SAY_ACCEPTED : str = "the review accepted"; +pub const SAY_MS : str = "ms since the event"; +pub const SAY_PENDING : str = "new cells waiting"; +pub const SAY_PLACED : str = "cells placed before"; +pub const SAY_SINCE : str = "seconds since the last batch"; +pub const SAY_FX_NAMES : [6]str = ["nothing", "START, white", "ACCEPT, gold", "SEND BACK, amber", "LOST, red", "CANCEL, grey"]; +pub const SAY_FLASH : str = "flash, of 1000"; +pub const SAY_DUE : str = "new cells are placed now"; +pub const SAY_NOT_DUE : str = "new cells wait for the next batch"; +pub const SAY_BATCH : str = "{0} waiting, {1} s of {2}"; +pub const SAY_NOTE : str = "A new cell rebuilds the whole scene, so cells are placed in batches at most every 300 s, and all at once when the page first opens. A task ended paints nothing: its ship flies home, which says it."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/hive.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test ten_kinds_and_their_clocks { + assert K_KINDS == 10; + assert K_FLASH_MS == 1800; + assert K_CELL_BATCH_SECONDS == 300; +} + +test the_sliders_cross_each_clock { + assert K_MS_MAX > K_FLASH_MS; + assert K_SINCE_MAX > K_CELL_BATCH_SECONDS; + assert K_PRESET_KIND[0] == 5; +} diff --git a/apps/website/public/widgets/hive-cell/index.html b/apps/website/public/widgets/hive-cell/index.html new file mode 100644 index 0000000000..52a41993fa --- /dev/null +++ b/apps/website/public/widgets/hive-cell/index.html @@ -0,0 +1,47 @@ + + + + + + +The comb answers: what an event does to a cell · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The comb answers: what an event does to a cell

+

Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is cell_effect, flash_alpha and cells_due, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 27 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-cell/logic.js b/apps/website/public/widgets/hive-cell/logic.js new file mode 100644 index 0000000000..e84d4b2f73 --- /dev/null +++ b/apps/website/public/widgets/hive-cell/logic.js @@ -0,0 +1,44 @@ +// Generated by scripts/t27-logic.mjs from hive.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 44417077b4fe76f32540d55d0b983a15ecf6cdef514ef8859165474719a0aa71 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-cell/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABGwRgA39/fwF/YAF/AX9gAn9/AX9gBH9/f38BfwMHBgABAgIBAwQFAXABAQEFAwEAEQYJAX8BQYCAwAALB28HBm1lbW9yeQIADXQyN19jZWxsc19kdWUAAA90MjdfZmxhc2hfYWxwaGEAAQ90MjdfY2VsbF9lZmZlY3QAAg50Mjdfc2hpcF9hbHBoYQADDXQyN19zaGlwX2tlcHQABA10Mjdfc2hpcF9tb2RlAAUK6wEGGAACQCAADQBBAA8LIAFBAXMgAkGrAktyCyUBAX9BACEBAkAgAEGHDksNAEHoByAAQegHbEGIDm5rIQELIAELMQEBf0EAIQICQCAAQQlLDQAgAC0AgIDAgAAiACAAQQMgAUEBcRsgAEECRxshAgsgAgssAAJAIABBAXENAEHoBw8LAkAgAUHfEk0NAEEADwtB6AcgAUHoB2xB4BJuawsIACAAQeASSQtCAQF/QQRBACADQQFxIgQbIQMCQCAEDQAgAEEBcQ0AQQMhAwJAAkAgAg4CAQIAC0EADwtBAUECIAFBAXEbIQMLIAMLCxMBAEGAgMAACwoBAAAEAAIABQAA"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function shipMode(queued, working, cell, departing) { + return W.t27_ship_mode(queued ? 1 : 0, working ? 1 : 0, int(cell, 0, 255, "ship_mode.cell"), departing ? 1 : 0) & 255; +} +function shipKept(msSinceLeft) { + return (W.t27_ship_kept(int(msSinceLeft, 0, 4294967295, "ship_kept.ms_since_left")) & 1) !== 0; +} +function shipAlpha(departing, msSinceLeft) { + return W.t27_ship_alpha(departing ? 1 : 0, int(msSinceLeft, 0, 4294967295, "ship_alpha.ms_since_left")) >>> 0; +} +function cellEffect(kind, accepted) { + return W.t27_cell_effect(int(kind, 0, 255, "cell_effect.kind"), accepted ? 1 : 0) & 255; +} +function flashAlpha(msSince) { + return W.t27_flash_alpha(int(msSince, 0, 4294967295, "flash_alpha.ms_since")) >>> 0; +} +function cellsDue(pending, placedBefore, secondsSinceBatch) { + return (W.t27_cells_due(int(pending, 0, 4294967295, "cells_due.pending"), placedBefore ? 1 : 0, int(secondsSinceBatch, 0, 4294967295, "cells_due.seconds_since_batch")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + cellEffect, + cellsDue, + flashAlpha, + shipAlpha, + shipKept, + shipMode +}; diff --git a/apps/website/public/widgets/hive-cell/logic.t27 b/apps/website/public/widgets/hive-cell/logic.t27 new file mode 100644 index 0000000000..ec636503bf --- /dev/null +++ b/apps/website/public/widgets/hive-cell/logic.t27 @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to +; a cell, how a ship leaves, and how often the map may grow +; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game +; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4. +; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day): +; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen. +; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far +; from the repository it works for. +; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole +; scene: one rebuild for most bees that started. +; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same +; as no news. +; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two +; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenHive; + +pub const HIVE_KINDS : u8 = 10; + +; --- 1. Where a ship flies --------------------------------------------------------------------------- + +/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this +/// map can show (a repository with no region, or a task that is not an issue). +pub const CELL_PLACED : u8 = 0; +pub const CELL_PENDING : u8 = 1; +pub const CELL_NONE : u8 = 2; + +/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim, +/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is +/// placed. HOME: flying back to the entrance, its task over. +pub const SM_PARK : u8 = 0; +pub const SM_ORBIT : u8 = 1; +pub const SM_HOVER : u8 = 2; +pub const SM_PORTAL : u8 = 3; +pub const SM_HOME : u8 = 4; + +/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no +/// longer in the feed. +pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 { + if (departing) { return SM_HOME; } + if (queued) { return SM_PARK; } + if (cell == CELL_PENDING) { return SM_PORTAL; } + if (cell != CELL_PLACED) { return SM_PARK; } + if (working) { return SM_ORBIT; } + return SM_HOVER; +} + +; --- 2. How a ship leaves ---------------------------------------------------------------------------- + +/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing. +pub const DEPART_MS : u32 = 2400; + +pub fn ship_kept(ms_since_left: u32) -> bool { + return ms_since_left < DEPART_MS; +} + +/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing. +pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 { + if (departing == false) { return 1000; } + if (ms_since_left >= DEPART_MS) { return 0; } + return 1000 - (ms_since_left * 1000) / DEPART_MS; +} + +; --- 3. What an event does to a cell ----------------------------------------------------------------- + +/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the +/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is +/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it. +pub const FX_NONE : u8 = 0; +pub const FX_START : u8 = 1; +pub const FX_ACCEPT : u8 = 2; +pub const FX_SEND_BACK : u8 = 3; +pub const FX_LOST : u8 = 4; +pub const FX_CANCEL : u8 = 5; + +/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by +/// its verdict, cancel CANCEL; everything else nothing. +pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0]; + +/// `accepted`: a review's verdict was an acceptance. Only a review reads it. +pub fn cell_effect(kind: u8, accepted: bool) -> u8 { + if (kind >= HIVE_KINDS) { return FX_NONE; } + let fx : u8 = FX_OF_KIND[kind]; + if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; } + return fx; +} + +/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn. +pub const FLASH_MS : u32 = 1800; + +pub fn flash_alpha(ms_since: u32) -> u32 { + if (ms_since >= FLASH_MS) { return 0; } + return 1000 - (ms_since * 1000) / FLASH_MS; +} + +; --- 4. How often the map may grow ------------------------------------------------------------------- + +/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding +/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS -- +/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of +/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's +/// portal (SM_PORTAL), so where it is going is already true. +pub const CELL_BATCH_SECONDS : u32 = 300; + +pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool { + if (pending == 0) { return false; } + if (placed_before == false) { return true; } + return seconds_since_batch >= CELL_BATCH_SECONDS; +} + +test a_ship_flies_where_its_bee_is { + // queued: at the entrance, wherever its cell stands + assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK; + assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK; + // claimed, its cell placed: orbit while heard, hover when quiet + assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT; + assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER; + // its cell not placed yet: at its repository's portal, working or not + assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL; + assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL; + // no cell this map can show: back at the entrance + assert ship_mode(false, true, CELL_NONE, false) == SM_PARK; + // gone from the feed: home, whatever it was + assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME; + assert ship_mode(true, false, CELL_NONE, true) == SM_HOME; +} + +test a_ship_leaves_by_flying_home { + assert ship_kept(0) == true; + assert ship_kept(DEPART_MS - 1) == true; + assert ship_kept(DEPART_MS) == false; + assert ship_alpha(false, 0) == 1000; + assert ship_alpha(false, 99999) == 1000; + assert ship_alpha(true, 0) == 1000; + assert ship_alpha(true, 1200) == 500; + assert ship_alpha(true, DEPART_MS - 1) == 1; + assert ship_alpha(true, DEPART_MS) == 0; + assert ship_alpha(true, 4000000000) == 0; +} + +test an_event_lights_its_cell { + assert cell_effect(0, false) == FX_START; + assert cell_effect(1, true) == FX_NONE; + assert cell_effect(2, false) == FX_NONE; + assert cell_effect(3, false) == FX_LOST; + assert cell_effect(4, true) == FX_NONE; + assert cell_effect(5, true) == FX_ACCEPT; + assert cell_effect(5, false) == FX_SEND_BACK; + assert cell_effect(6, true) == FX_NONE; + assert cell_effect(7, false) == FX_CANCEL; + assert cell_effect(8, false) == FX_NONE; + assert cell_effect(9, true) == FX_NONE; + // only a review reads the verdict + assert cell_effect(3, true) == FX_LOST; + assert cell_effect(HIVE_KINDS, true) == FX_NONE; +} + +test a_flash_fades { + assert flash_alpha(0) == 1000; + assert flash_alpha(900) == 500; + assert flash_alpha(FLASH_MS - 1) == 1; + assert flash_alpha(FLASH_MS) == 0; + assert flash_alpha(4000000000) == 0; +} + +test the_map_grows_in_batches { + assert cells_due(0, false, 0) == false; + assert cells_due(0, true, 99999) == false; + // the first answer places every cell at once + assert cells_due(3, false, 0) == true; + // after that, one batch a period + assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false; + assert cells_due(1, true, CELL_BATCH_SECONDS) == true; +} + +invariant one_row_per_kind { + assert HIVE_KINDS == 10; + assert FX_OF_KIND[5] == FX_ACCEPT; + assert FLASH_MS < DEPART_MS * 2; + assert CELL_BATCH_SECONDS == 300; +} diff --git a/apps/website/public/widgets/hive-cell/tool.js b/apps/website/public/widgets/hive-cell/tool.js new file mode 100644 index 0000000000..08476448c0 --- /dev/null +++ b/apps/website/public/widgets/hive-cell/tool.js @@ -0,0 +1,32 @@ +// hive-cell/tool.js -- lesson 27 of queen-and-hive: what an event does to a cell of the comb. Each cell +// is cell_effect(kind, accepted) drawn at flash_alpha(ms), and the gate is cells_due, fn bodies of +// gHashTag/t27 specs/queen/hive.t27 compiled to wasm (./logic.js); K_FX_TONES only picks a colour for +// each effect code. Words are SAY_ and numbers K_ constants of specs/widgets/hive-cell.t27. +import { cellEffect, cellsDue, flashAlpha } from './logic.js' +import { K, W, ask, comb, controls, el, fill, gate, hue, root, say, source, verdict } from '../hive-kit.js' + +const NK = K('K_KINDS', 10), FX = K('K_FX_TONES', []) +const ctl = controls([ + { key: 'kind', kind: 'hidden', min: 0, max: NK - 1 }, + { key: 'ms', label: say('SAY_MS'), min: 0, max: K('K_MS_MAX', 2000), step: K('K_MS_STEP', 50) }, + { key: 'accepted', label: say('SAY_ACCEPTED'), kind: 'toggle' }, + { key: 'pending', label: say('SAY_PENDING'), min: 0, max: K('K_PENDING_MAX', 10), step: 1 }, + { key: 'since', label: say('SAY_SINCE'), min: 0, max: K('K_SINCE_MAX', 600), step: K('K_SINCE_STEP', 10) }, + { key: 'placed', label: say('SAY_PLACED'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { kind: K('K_PRESET_KIND', []), accepted: K('K_PRESET_ACCEPTED', []), ms: K('K_PRESET_MS', []), pending: K('K_PRESET_PENDING', []), placed: K('K_PRESET_PLACED', []), since: K('K_PRESET_SINCE', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const alpha = flashAlpha(s.ms) + const fxOf = (k) => cellEffect(k, !!s.accepted) + const fx = fxOf(s.kind) + const due = cellsDue(s.pending, !!s.placed, s.since) + out.replaceChildren( + comb([...Array(NK)].map((_, k) => { const f = fxOf(k); return { label: say('SAY_KIND_SHORT', k), fill: f ? hue(FX[f]) : null, alpha: k === s.kind ? alpha / 1000 : f ? 0.35 : 0, on: k === s.kind } }), (k) => ctl.set('kind', k)), + verdict(`${say('SAY_KIND_NAMES', s.kind)}: ${say('SAY_FX_NAMES', fx)}`, fx ? FX[fx] : 8, `${say('SAY_FLASH')}: ${alpha}`), + gate(due, due ? say('SAY_DUE') : say('SAY_NOT_DUE'), fill(say('SAY_BATCH'), s.pending, s.since, K('K_CELL_BATCH_SECONDS', 300)))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-chain/card.png b/apps/website/public/widgets/hive-chain/card.png new file mode 100644 index 0000000000..5a4e1eb05b Binary files /dev/null and b/apps/website/public/widgets/hive-chain/card.png differ diff --git a/apps/website/public/widgets/hive-chain/hive-chain.t27 b/apps/website/public/widgets/hive-chain/hive-chain.t27 new file mode 100644 index 0000000000..0fcd1288b6 --- /dev/null +++ b/apps/website/public/widgets/hive-chain/hive-chain.t27 @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-chain.t27 -- Chain depth: human, A, B, C, then silence +; Source of truth for public/widgets/hive-chain/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-chain/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 23 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the milliseconds between one bot message and the next. The +; person's rung is may_answer(from a person); each agent rung is may_answer(from a bot, depth, ms) +; at its depth, and chain_open(depth) beside it. + +; WHAT IT DOES NOT CLAIM: it is a ladder of turns, not a live chat. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_chain; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-chain"; +pub const TITLE : str = "Chain depth: human, A, B, C, then silence"; +pub const DESCRIPTION : str = "One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it."; +pub const IMAGE_ALT : str = "Chain card: a person asks and agents answer 6 s apart. A ladder of six rungs: the person, agent A, agent B and agent C are lit green; agent D and agent E are grey, silent at depth 3. Tiles read depth limit 3 and cooldown 5000 ms."; +pub const HOOK : str = "Telegram has no loop protection between bots. Two numbers stop two agents answering each other forever: a depth of 3 and a cooldown of 5 s."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is chain_open and may_answer, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 23 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_GAP_MAX : u16 = 10000; +pub const K_GAP_STEP : u8 = 100; +pub const K_RUNGS : u8 = 6; +pub const K_MAX_BOT_DEPTH : u32 = 3; +pub const K_BOT_REPLY_COOLDOWN_MS : u32 = 5000; +pub const K_PRESET_GAP : [3]u16 = [6000, 3200, 5000]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["6 s apart", "3.2 s apart", "Exactly 5 s"]; +pub const SAY_ASK : str = "Two agents can answer each other. What stops them from doing it forever?"; +pub const SAY_GAP : str = "ms between one bot message and the next"; +pub const SAY_RUNG_NAMES : [6]str = ["a person asks", "agent A", "agent B", "agent C", "agent D", "agent E"]; +pub const SAY_DEPTH : str = "depth {0}"; +pub const SAY_OPEN : str = "chain open"; +pub const SAY_CLOSED : str = "chain closed"; +pub const SAY_ANSWERS : str = "answers"; +pub const SAY_SILENT : str = "silent"; +pub const SAY_LIMIT : str = "depth limit"; +pub const SAY_COOL : str = "cooldown, ms"; +pub const SAY_LAST : str = "last to speak"; +pub const SAY_NOTE : str = "Depth counts the bot messages already between this one and the person. A person is always answered, whatever the depth; a bot only inside the chain and after the cooldown."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test three_deep_five_seconds { + assert K_MAX_BOT_DEPTH == 3; + assert K_BOT_REPLY_COOLDOWN_MS == 5000; + assert K_RUNGS > K_MAX_BOT_DEPTH + 1; +} + +test the_start_is_past_the_cooldown { + assert K_PRESET_GAP[0] > K_BOT_REPLY_COOLDOWN_MS; + assert K_PRESET_GAP[2] == K_BOT_REPLY_COOLDOWN_MS; + assert K_PRESET_GAP[0] <= K_GAP_MAX; +} diff --git a/apps/website/public/widgets/hive-chain/index.html b/apps/website/public/widgets/hive-chain/index.html new file mode 100644 index 0000000000..49aa820837 --- /dev/null +++ b/apps/website/public/widgets/hive-chain/index.html @@ -0,0 +1,47 @@ + + + + + + +Chain depth: human, A, B, C, then silence · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Chain depth: human, A, B, C, then silence

+

One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is chain_open and may_answer, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 23 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-chain/logic.js b/apps/website/public/widgets/hive-chain/logic.js new file mode 100644 index 0000000000..c8cc5d792c --- /dev/null +++ b/apps/website/public/widgets/hive-chain/logic.js @@ -0,0 +1,116 @@ +// Generated by scripts/t27-logic.mjs from agent-hive-group.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 9cdd5ed3872b60e900a364d843f908920334408f965bde194d233b64534076f9 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-chain/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJAVgAX8Bf2ACf38Bf2ADf39/AX9gBH9/f38Bf2AFf39/f38BfwMZGAABAAACAAIBAAEAAQEAAQADAAAAAgAABAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB74DGQZtZW1vcnkCABR0MjdfZG1faGFuZG9mZnNfc2VudAAADXQyN19kbV90YWdfb2sAAQ50MjdfZG1fY2FycmllcwACEHQyN19kbV9ydW5zX3R1cm4AAwx0MjdfZG1faGVhcmQABBF0Mjdfc3VtbWFyeV9jb3VudAAFEnQyN19yZWNyZWF0ZV90b3BpYwAGEXQyN19tYXlfc2V0X2JyaWVmAAcOdDI3X2JyaWVmX2ZpdHMACBB0MjdfYnJpZWZfc291cmNlAAkTdDI3X2tlZXBzX3NlZW5fbWFyawAKC3QyN190dXJuX29rAAsVdDI3X2hlYXJkX3doaWxlX211dGVkAAwJdDI3X211dGVkAA0ZdDI3X21heV9hbnN3ZXJfYWZ0ZXJfc3RvcAAOC3QyN19zdG9wcGVkAA8OdDI3X2ZvbGxvd3NfdXAAEBF0Mjdfc3BlYWtzX29uX2FsbAARDnQyN190eXBpbmdfZHVlABIPdDI3X2NsYWltX2ZyZXNoABMOdDI3X21heV9hbnN3ZXIAFAp0MjdfY29vbGVkABUOdDI3X2NoYWluX29wZW4AFg10MjdfYWRkcmVzc2VkABcK7AIYDAAgAEECIABBAkkbCyoBAX9BASECAkACQAJAIABBf2oOAgIAAQsgAUHA7u0ASQ8LQQAhAgsgAgsHACAAQQJGCwcAIABBAUYLDQAgAEEBcyABIAJxcgsTACAAQcgBIABByAFJG0EyIAAbCxIAIAAgAXFBf3MgAkG/zyRLcQsKACAAIAFBAXNxCwgAIABB6QdJCw8AQQIgAUEBcSAAQQFxGwsEACAACwQAIAALDQAgACABQf/c2wFLcgsKACAAQYDd2wFJCw8AIABBAXMgAUG/zyRLcgsJACAAQcDPJEkLFQAgAEF/cyABcSACcSADQcCpB0lxCwUAIABFCwgAIABBnx9LCwkAIABBoPc2SQsjAQF/QQEhAwJAIABBAXFFDQAgAUEDSSACQYcnS3EhAwsgAwsIACAAQYcnSwsHACAAQQNJCygBAX9BASEFAkAgAEEBcQ0AIAJBAXENACAEQQFzIAEgA3JxIQULIAUL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function addressed(tagged, allTag, replyToMe, ownTopic, fromBot) { + return (W.t27_addressed(tagged ? 1 : 0, allTag ? 1 : 0, replyToMe ? 1 : 0, ownTopic ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function chainOpen(depth) { + return (W.t27_chain_open(int(depth, 0, 4294967295, "chain_open.depth")) & 1) !== 0; +} +function cooled(sinceLastMs) { + return (W.t27_cooled(int(sinceLastMs, 0, 4294967295, "cooled.since_last_ms")) & 1) !== 0; +} +function mayAnswer(fromBot, depth, sinceLastMs) { + return (W.t27_may_answer(fromBot ? 1 : 0, int(depth, 0, 4294967295, "may_answer.depth"), int(sinceLastMs, 0, 4294967295, "may_answer.since_last_ms")) & 1) !== 0; +} +function claimFresh(ageMs) { + return (W.t27_claim_fresh(int(ageMs, 0, 4294967295, "claim_fresh.age_ms")) & 1) !== 0; +} +function typingDue(sinceLastMs) { + return (W.t27_typing_due(int(sinceLastMs, 0, 4294967295, "typing_due.since_last_ms")) & 1) !== 0; +} +function speaksOnAll(rank) { + return (W.t27_speaks_on_all(int(rank, 0, 4294967295, "speaks_on_all.rank")) & 1) !== 0; +} +function followsUp(fromBot, samePerson, sameThread, ageMs) { + return (W.t27_follows_up(fromBot ? 1 : 0, samePerson ? 1 : 0, sameThread ? 1 : 0, int(ageMs, 0, 4294967295, "follows_up.age_ms")) & 1) !== 0; +} +function stopped(sinceStopMs) { + return (W.t27_stopped(int(sinceStopMs, 0, 4294967295, "stopped.since_stop_ms")) & 1) !== 0; +} +function mayAnswerAfterStop(fromBot, sinceStopMs) { + return (W.t27_may_answer_after_stop(fromBot ? 1 : 0, int(sinceStopMs, 0, 4294967295, "may_answer_after_stop.since_stop_ms")) & 1) !== 0; +} +function muted(sinceMuteMs) { + return (W.t27_muted(int(sinceMuteMs, 0, 4294967295, "muted.since_mute_ms")) & 1) !== 0; +} +function heardWhileMuted(tagged, sinceMuteMs) { + return (W.t27_heard_while_muted(tagged ? 1 : 0, int(sinceMuteMs, 0, 4294967295, "heard_while_muted.since_mute_ms")) & 1) !== 0; +} +function turnOk(answerSent, reactionOk) { + return (W.t27_turn_ok(answerSent ? 1 : 0, reactionOk ? 1 : 0) & 1) !== 0; +} +function keepsSeenMark(answerSent) { + return (W.t27_keeps_seen_mark(answerSent ? 1 : 0) & 1) !== 0; +} +function briefSource(hasTopicBrief, hasGroupBrief) { + return W.t27_brief_source(hasTopicBrief ? 1 : 0, hasGroupBrief ? 1 : 0) & 255; +} +function briefFits(chars) { + return (W.t27_brief_fits(int(chars, 0, 4294967295, "brief_fits.chars")) & 1) !== 0; +} +function maySetBrief(isAdmin, fromBot) { + return (W.t27_may_set_brief(isAdmin ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function recreateTopic(stored, threadFound, sinceLastTryMs) { + return (W.t27_recreate_topic(stored ? 1 : 0, threadFound ? 1 : 0, int(sinceLastTryMs, 0, 4294967295, "recreate_topic.since_last_try_ms")) & 1) !== 0; +} +function summaryCount(asked) { + return W.t27_summary_count(int(asked, 0, 4294967295, "summary_count.asked")) >>> 0; +} +function dmHeard(fromBot, colleague, tagOk) { + return (W.t27_dm_heard(fromBot ? 1 : 0, colleague ? 1 : 0, tagOk ? 1 : 0) & 1) !== 0; +} +function dmRunsTurn(kind) { + return (W.t27_dm_runs_turn(int(kind, 0, 255, "dm_runs_turn.kind")) & 1) !== 0; +} +function dmCarries(kind) { + return (W.t27_dm_carries(int(kind, 0, 255, "dm_carries.kind")) & 1) !== 0; +} +function dmTagOk(kind, sinceAskMs) { + return (W.t27_dm_tag_ok(int(kind, 0, 255, "dm_tag_ok.kind"), int(sinceAskMs, 0, 4294967295, "dm_tag_ok.since_ask_ms")) & 1) !== 0; +} +function dmHandoffsSent(asked) { + return W.t27_dm_handoffs_sent(int(asked, 0, 4294967295, "dm_handoffs_sent.asked")) >>> 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + addressed, + briefFits, + briefSource, + chainOpen, + claimFresh, + cooled, + dmCarries, + dmHandoffsSent, + dmHeard, + dmRunsTurn, + dmTagOk, + followsUp, + heardWhileMuted, + keepsSeenMark, + mayAnswer, + mayAnswerAfterStop, + maySetBrief, + muted, + recreateTopic, + speaksOnAll, + stopped, + summaryCount, + turnOk, + typingDue +}; diff --git a/apps/website/public/widgets/hive-chain/logic.t27 b/apps/website/public/widgets/hive-chain/logic.t27 new file mode 100644 index 0000000000..2f801b0db7 --- /dev/null +++ b/apps/website/public/widgets/hive-chain/logic.t27 @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group. +// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots). +// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group". +// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/. +// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode +// switched on in @BotFather for every bot, which no API call can do. +// Telegram gives no loop protection between bots, so the depth and the cooldown +// below are the only thing that stops two agents from answering each other forever. +// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window. +// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept +// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute. +// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs, +// a topic a person deleted, and /summary. +// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private +// message to another bot by @username when Bot-to-Bot Communication Mode is on for both +// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the +// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn, +// and only while its ask is fresh. +// Earlier laws are unchanged; each version only adds, so a host on an older one stays true. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::agent_hive_group { + pub const KIND : str = "automation"; + pub const ID : str = "agent-hive-group"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 4; + + // Bot messages in a row after one human message: human -> A -> B -> C, then silence. + pub const MAX_BOT_DEPTH : u32 = 3; + // One agent answers another agent at most this often. + pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000; + // How long "create the bot for agent X" waits for Telegram's creation message. + pub const CLAIM_TTL_MS : u32 = 900000; + // The tag that calls every agent of the hive at once. + pub const ALL_TAG : str = "@all"; + // Suggested username of an agent's bot: t27__bot. + pub const BOT_USERNAME_PREFIX : str = "t27_"; + pub const BOT_USERNAME_SUFFIX : str = "_bot"; + + // A person may call an agent by its tag, by the all tag, by replying to it, + // or by writing in the agent's own topic. Another bot only by tag or reply: + // a bot never triggers the whole hive, and never wakes a topic's owner by + // merely writing there. + fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool { + return tagged || reply_to_me || (!from_bot && (all_tag || own_topic)); + } + + // depth = how many bot messages already stand between this one and the person. + fn chain_open(depth: u32) -> bool { + return depth < MAX_BOT_DEPTH; + } + + fn cooled(since_last_ms: u32) -> bool { + return since_last_ms >= BOT_REPLY_COOLDOWN_MS; + } + + // A person is always answered; a bot only inside the chain and after the cooldown. + fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool { + return !from_bot || (chain_open(depth) && cooled(since_last_ms)); + } + + fn claim_fresh(age_ms: u32) -> bool { + return age_ms < CLAIM_TTL_MS; + } + + // ---- v2 ---- + + // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute. + // The action is sent again this long after the last one, until the answer goes out. + pub const TYPING_REFRESH_MS : u32 = 4000; + // On the all tag only this many agents answer at once, best fit first; the rest + // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team. + pub const ALL_FIRST_SPEAKERS : u32 = 1; + // A person's next message in the same topic goes to the agent that answered + // last, without a new tag, for this long. + pub const FOLLOW_UP_WINDOW_MS : u32 = 120000; + // /stop: no bot answers a bot in that chat for this long. People are still answered. + pub const STOP_WINDOW_MS : u32 = 600000; + // /mute: the agent answers in that topic only when tagged by name, for this long. + pub const MUTE_MS : u32 = 3600000; + + fn typing_due(since_last_ms: u32) -> bool { + return since_last_ms >= TYPING_REFRESH_MS; + } + + // rank = the agent's place in the best-fit order for this message, from 0. + fn speaks_on_all(rank: u32) -> bool { + return rank < ALL_FIRST_SPEAKERS; + } + + // A bot never follows up: it must tag or reply, as v1 says. + fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool { + return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS; + } + + fn stopped(since_stop_ms: u32) -> bool { + return since_stop_ms < STOP_WINDOW_MS; + } + + // A stop cuts bot-to-bot turns only; a person is always heard. + fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool { + return !from_bot || !stopped(since_stop_ms); + } + + fn muted(since_mute_ms: u32) -> bool { + return since_mute_ms < MUTE_MS; + } + + // A muted agent still answers its own tag: muting a topic must not lose a direct call. + fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool { + return tagged || !muted(since_mute_ms); + } + + // ---- v3 ---- + + // A reaction is a courtesy: chats may switch reactions off, so the answer counts + // whether or not the reaction went through. The "seen" mark stays only when the + // turn ended in an answer; a failed turn takes it away so nobody waits for nothing. + fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool { + return answer_sent; + } + + fn keeps_seen_mark(answer_sent: bool) -> bool { + return answer_sent; + } + + // /brief: standing instructions for a group or one topic, set by an admin only. + pub const MAX_BRIEF_CHARS : u32 = 1000; + // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's. + // The narrower one wins; the two are never glued together. + fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 { + if has_topic_brief { return 2; } + if has_group_brief { return 1; } + return 0; + } + + // An empty /brief is accepted: it clears the brief, it is not refused. + fn brief_fits(chars: u32) -> bool { + return chars <= MAX_BRIEF_CHARS; + } + + fn may_set_brief(is_admin: bool, from_bot: bool) -> bool { + return is_admin && !from_bot; + } + + // A person deleted an agent's topic: the stored id is stale, so it is recreated. + // Without rights the recreate fails every time, so it is tried at most this often. + pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000; + fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool { + return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS; + } + + // /summary: the topic keeps a bounded log; a bare /summary reads the default count. + pub const SUMMARY_LOG_SIZE : u32 = 200; + pub const SUMMARY_DEFAULT : u32 = 50; + fn summary_count(asked: u32) -> u32 { + if asked == 0 { return SUMMARY_DEFAULT; } + if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; } + return asked; + } + + // ---- v4: hand-off in private, bot to bot ---- + + // Every private hand-off between two agents starts with this tag, then its kind and the + // person it is for. Only a hive agent of the same owner can send it, so the person named in + // it is trusted: the turn runs in that person's name, never the owner's. + pub const DM_TAG : str = "#a2a"; + pub const DM_ASK : u8 = 1; + pub const DM_ANSWER : u8 = 2; + // An answer to a person hands off to at most this many colleagues. + pub const MAX_DM_HANDOFFS : u32 = 2; + + // In private a person is always heard; a bot only when it is a colleague (an agent bot of + // the same owner) and its message carries the hand-off tag with a known kind. + fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool { + return !from_bot || (colleague && tag_ok); + } + + // An ask runs the colleague's turn; an answer is only carried to the person. So a private + // chain is two messages long and cannot loop, whatever the agents write. + fn dm_runs_turn(kind: u8) -> bool { + return kind == DM_ASK; + } + fn dm_carries(kind: u8) -> bool { + return kind == DM_ANSWER; + } + + // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never + // writes to a person nobody asked it about. Half an hour covers the longest turn. + pub const DM_ANSWER_WINDOW_MS : u32 = 1800000; + fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool { + if kind == DM_ASK { return true; } + if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; } + return false; + } + + // How many of the hand-offs an answer asked for are sent. + fn dm_handoffs_sent(asked: u32) -> u32 { + if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; } + return asked; + } + + test "a person calls an agent by tag, all tag, reply or its own topic" { + assert(addressed(true, false, false, false, false)); + assert(addressed(false, true, false, false, false)); + assert(addressed(false, false, true, false, false)); + assert(addressed(false, false, false, true, false)); + assert(!addressed(false, false, false, false, false)); + } + + test "a bot calls an agent only by tag or reply" { + assert(addressed(true, false, false, false, true)); + assert(addressed(false, false, true, false, true)); + assert(!addressed(false, true, false, false, true)); + assert(!addressed(false, false, false, true, true)); + assert(!addressed(false, true, false, true, true)); + } + + test "the bot chain closes after three bot messages" { + assert(chain_open(0)); + assert(chain_open(2)); + assert(!chain_open(3)); + assert(!chain_open(4)); + } + + test "a bot waits out the cooldown before answering a bot" { + assert(!cooled(0)); + assert(!cooled(4999)); + assert(cooled(5000)); + } + + test "a person is always answered and a bot only inside the limits" { + assert(may_answer(false, 9, 0)); + assert(may_answer(true, 2, 5000)); + assert(!may_answer(true, 3, 5000)); + assert(!may_answer(true, 2, 4999)); + } + + test "a creation claim lives fifteen minutes" { + assert(claim_fresh(0)); + assert(claim_fresh(899999)); + assert(!claim_fresh(900000)); + } + + test "typing is sent again four seconds after the last one" { + assert(!typing_due(0)); + assert(!typing_due(3999)); + assert(typing_due(4000)); + } + + test "on the all tag only the best fit answers at once" { + assert(speaks_on_all(0)); + assert(!speaks_on_all(1)); + assert(!speaks_on_all(7)); + } + + test "a person follows up in the same topic for two minutes without a tag" { + assert(follows_up(false, true, true, 0)); + assert(follows_up(false, true, true, 119999)); + assert(!follows_up(false, true, true, 120000)); + assert(!follows_up(false, false, true, 0)); + assert(!follows_up(false, true, false, 0)); + assert(!follows_up(true, true, true, 0)); + } + + test "a stop silences bots for ten minutes and never a person" { + assert(!may_answer_after_stop(true, 0)); + assert(!may_answer_after_stop(true, 599999)); + assert(may_answer_after_stop(true, 600000)); + assert(may_answer_after_stop(false, 0)); + } + + test "a muted agent still answers its own tag for the hour" { + assert(heard_while_muted(true, 0)); + assert(!heard_while_muted(false, 0)); + assert(!heard_while_muted(false, 3599999)); + assert(heard_while_muted(false, 3600000)); + } + + test "the answer counts even when the chat refuses reactions" { + assert(turn_ok(true, false)); + assert(turn_ok(true, true)); + assert(!turn_ok(false, true)); + assert(keeps_seen_mark(true)); + assert(!keeps_seen_mark(false)); + } + + test "the topic brief wins over the group brief, and only an admin sets one" { + assert(brief_source(true, true) == 2); + assert(brief_source(true, false) == 2); + assert(brief_source(false, true) == 1); + assert(brief_source(false, false) == 0); + assert(brief_fits(0)); + assert(brief_fits(1000)); + assert(!brief_fits(1001)); + assert(may_set_brief(true, false)); + assert(!may_set_brief(false, false)); + assert(!may_set_brief(true, true)); + } + + test "a deleted topic is recreated, at most once in ten minutes" { + assert(recreate_topic(false, false, 600000)); + assert(recreate_topic(false, true, 600000)); + assert(recreate_topic(true, false, 600000)); + assert(!recreate_topic(true, true, 600000)); + assert(!recreate_topic(true, false, 599999)); + } + + test "a summary reads fifty by default and never past the log" { + assert(summary_count(0) == 50); + assert(summary_count(1) == 1); + assert(summary_count(10) == 10); + assert(summary_count(200) == 200); + assert(summary_count(201) == 200); + } + + test "in private a colleague is heard only with the hand-off tag" { + assert(dm_heard(false, false, false)); + assert(dm_heard(true, true, true)); + assert(!dm_heard(true, false, true)); + assert(!dm_heard(true, true, false)); + } + + test "an ask runs a turn, an answer is only carried" { + assert(DM_ASK == 1); + assert(DM_ANSWER == 2); + assert(dm_runs_turn(DM_ASK)); + assert(!dm_runs_turn(DM_ANSWER)); + assert(!dm_runs_turn(0)); + assert(dm_carries(DM_ANSWER)); + assert(!dm_carries(DM_ASK)); + assert(!dm_carries(0)); + } + + test "an answer is carried only while its ask is fresh" { + assert(DM_ANSWER_WINDOW_MS == 1800000); + assert(dm_tag_ok(DM_ASK, 4000000000)); + assert(dm_tag_ok(DM_ANSWER, 0)); + assert(dm_tag_ok(DM_ANSWER, 1799999)); + assert(!dm_tag_ok(DM_ANSWER, 1800000)); + assert(!dm_tag_ok(DM_ANSWER, 4000000000)); + assert(!dm_tag_ok(0, 0)); + assert(!dm_tag_ok(3, 0)); + } + + test "an answer hands off to at most two colleagues" { + assert(MAX_DM_HANDOFFS == 2); + assert(dm_handoffs_sent(0) == 0); + assert(dm_handoffs_sent(1) == 1); + assert(dm_handoffs_sent(2) == 2); + assert(dm_handoffs_sent(3) == 2); + assert(dm_handoffs_sent(100) == 2); + } + + test "VERSION says what the header's last note says" { + assert(VERSION == 4); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-chain/tool.js b/apps/website/public/widgets/hive-chain/tool.js new file mode 100644 index 0000000000..f5e25d777a --- /dev/null +++ b/apps/website/public/widgets/hive-chain/tool.js @@ -0,0 +1,30 @@ +// hive-chain/tool.js -- lesson 23 of queen-and-hive: human, A, B, C, then silence. The person's rung is +// may_answer from a person and every agent rung may_answer from a bot at its depth, with chain_open +// beside it: fn bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm +// (./logic.js). A rung speaks only if the one above it spoke: an agent answers a message that exists. +// Words are SAY_ and numbers K_ constants of specs/widgets/hive-chain.t27. +import { chainOpen, mayAnswer } from './logic.js' +import { K, W, ask, controls, el, fill, ladder, root, say, source, tiles } from '../hive-kit.js' + +const ctl = controls([{ key: 'gap', label: say('SAY_GAP'), min: 0, max: K('K_GAP_MAX', 10000), step: K('K_GAP_STEP', 100) }], { names: W.SAY_PRESET_NAMES, values: { gap: K('K_PRESET_GAP', []) } }, draw) +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + let spoke = true, last = 0 + const rungs = [...Array(K('K_RUNGS', 6))].map((_, k) => { + if (k === 0) return { label: say('SAY_RUNG_NAMES', 0), sub: say('SAY_ANSWERS'), tone: 2, on: true } + const depth = k - 1 + const may = k === 1 ? mayAnswer(false, 0, s.gap) : mayAnswer(true, depth, s.gap) + spoke = spoke && may + if (spoke) last = k + return { label: say('SAY_RUNG_NAMES', k), sub: `${fill(say('SAY_DEPTH'), depth)} · ${chainOpen(depth) ? say('SAY_OPEN') : say('SAY_CLOSED')} \u00b7 ${spoke ? say('SAY_ANSWERS') : say('SAY_SILENT')}`, tone: spoke ? 0 : 8, on: spoke } + }) + out.replaceChildren(ladder(rungs), tiles([ + { label: say('SAY_LAST'), value: say('SAY_RUNG_NAMES', last), tone: 0 }, + { label: say('SAY_LIMIT'), value: String(K('K_MAX_BOT_DEPTH', 3)), tone: 1 }, + { label: say('SAY_COOL'), value: String(K('K_BOT_REPLY_COOLDOWN_MS', 5000)), tone: 2 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-column/card.png b/apps/website/public/widgets/hive-column/card.png new file mode 100644 index 0000000000..3049e8bee4 Binary files /dev/null and b/apps/website/public/widgets/hive-column/card.png differ diff --git a/apps/website/public/widgets/hive-column/hive-column.t27 b/apps/website/public/widgets/hive-column/hive-column.t27 new file mode 100644 index 0000000000..45a8b97ca9 --- /dev/null +++ b/apps/website/public/widgets/hive-column/hive-column.t27 @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-column.t27 -- The public board: which column a task's card is in +; Source of truth for public/widgets/hive-column/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-column/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/dispatch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 26 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader says whether the issue is open and whether the issue list was read. Each +; of five claim cards sits in the column column_of(claim, issue open, list known) names. + +; WHAT IT DOES NOT CLAIM: it reads no board: the five claims are every claim the spec has. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_column; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-column"; +pub const TITLE : str = "The public board: which column a task's card is in"; +pub const DESCRIPTION : str = "Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict."; +pub const IMAGE_ALT : str = "Board card: the issue is closed and the issue list was read. Of six columns, running holds the running claim and done holds four cards, accepted, rejected, awaiting review and failed; backlog, blocked, review and dropped are empty."; +pub const HOOK : str = "The spec records it as a finding: the public board draws a closed issue's dispatch as done whatever its verdict, an escalation and a send-back included."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is column_of, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 26 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CLAIMS : u8 = 5; +pub const K_COLUMNS : u8 = 6; +pub const K_COL_DONE : u32 = 4; +pub const K_PRESET_OPEN : [3]u8 = [0, 1, 0]; +pub const K_PRESET_KNOWN : [3]u8 = [1, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["Issue closed", "Issue open", "List not read"]; +pub const SAY_ASK : str = "A task was reviewed and its issue was closed. Where does the public board draw it?"; +pub const SAY_OPEN : str = "the issue is open"; +pub const SAY_KNOWN : str = "the issue list was read"; +pub const SAY_COLUMN_NAMES : [6]str = ["backlog", "blocked", "running", "review", "done", "dropped"]; +pub const SAY_CLAIM_NAMES : [5]str = ["running", "accepted", "rejected", "awaiting review", "failed"]; +pub const SAY_CLAIM : str = "claim"; +pub const SAY_IN_DONE : str = "{0} of {1} cards in done"; +pub const SAY_NOTE : str = "Done should mean accepted or merged. Until the board reads verdicts for closed issues, a send-back on a closed issue looks finished on the board; the spec names it so a reader is not misled."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/dispatch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test five_claims_six_columns { + assert K_CLAIMS == 5; + assert K_COLUMNS == 6; +} + +test the_start_is_a_closed_issue { + assert K_PRESET_OPEN[0] == 0; + assert K_PRESET_KNOWN[0] == 1; +} diff --git a/apps/website/public/widgets/hive-column/index.html b/apps/website/public/widgets/hive-column/index.html new file mode 100644 index 0000000000..7f329f511a --- /dev/null +++ b/apps/website/public/widgets/hive-column/index.html @@ -0,0 +1,47 @@ + + + + + + +The public board: which column a task's card is in · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The public board: which column a task's card is in

+

Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is column_of, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 26 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-column/logic.js b/apps/website/public/widgets/hive-column/logic.js new file mode 100644 index 0000000000..adca79a1af --- /dev/null +++ b/apps/website/public/widgets/hive-column/logic.js @@ -0,0 +1,123 @@ +// Generated by scripts/t27-logic.mjs from dispatch.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: e71997bc00e5e3c6fffc1d92d0ea1e73c09326215533f7bdec6a27ed9fffd7fe +// Imported by scripts/widget-logic.mjs for public/widgets/hive-column/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABQwpgBH9/f38Bf2ABfwF/YAF/AGAAAX9gAn9/AX9gBn9/f39/fwF/YAV/f39/fwF/YAN/f38Bf2ACf38AYAV/f39/fwADGhkBAQIBAwEBAwQBAQUEBgcBBAMICQEAAQUFBAUBcAEFBQUDAQARBgkBfwFBgIDAAAsHyQMWBm1lbW9yeQIAF3QyN19pc19jcmFzaF9lbmRfcmVhc29uAAAZdDI3X2lzX25vX2RpZmZfZW5kX3JlYXNvbgABFnQyN19lbmRfcmVhc29uX3RvX25hbWUAAhd0MjdfaXNfdmFsaWRfZW5kX3JlYXNvbgADGXQyN193cml0ZXJfZW5mb3JjZXNfdGFibGUABCB0MjdfYWNjZXB0X3JlcXVpcmVzX3B1bGxfcmVxdWVzdAAEE3QyN19mb2xsb3d1cF9yb3VuZHMABRJ0Mjdfc3RhbGxfcmVhcGFibGUABhN0Mjdfb3duZXJzX3Blcl90ZXJtAAcPdDI3X2xlYXNlX3Rha2VuAAgYdDI3X2NvdW50c19hZ2FpbnN0X2lzc3VlAAkRdDI3X3JldHJ5X2FsbG93ZWQACg10MjdfcmV2aWV3X29mAAsNdDI3X2Nhbl9zdGFydAAMDXQyN19jaG9zZW5fb2YADQ10MjdfY29sdW1uX29mAA4adDI3X3JlbGVhc2VzX3JlYWRfYnlfcm91bmQADw50MjdfbmV4dF9zdGF0ZQAQC3QyN19fcmV0X2F0ABEJdDI3X19mcmVlABIKdDI3X19hbGxvYwAUCQoBAEEBCwQVGBcTCvoLGQcAIABBAkYLEgAgAEEHSUH7ACAAQf8AcXZxC1ABAX8CQAJAIABBBk0NAEESIQBByYDAgAAhAQwBCyAAQQN0IgEoAvSAwIAAIQAgASgC8IDAgAAhAQtBACABNgKEgsCAAEEAIAA2AoiCwIAACwcAIABBB0kLBABBAAsHACAAQQBHCwgAIABB+ABLCwQAQQELBwAgACABcgsHACAAQQFzCwcAIABBAkkLPQACQCAADQBBAg8LAkAgASAATw0AQQMPCwJAIAIgAEcNAEEAQQIgBBtBAiADQQFxGw8LQQJBASAFQQFLGwsHACAAIAFJCz0AIABBf3NBAXFBAkEDIAJBAXEiAhsgACABckEBcSIBGyIAIAAgAEEEQQUgBEEBcRsgA0EBcRsgAhsgARsLNQACQCAARQ0AIAJBAXFFDQAgAUEBcQ0AQQQPCwJAIABBA00NAEEFDwsgAEECdCgCwIHAgAALBABBAAu4AQEBf0HjACECAkACQAJAAkACQAJAAkACQCAADgwABQQGBgECAwIEBAQHC0ECQeMAIAFBAUYbQQEgARsPC0EGQQdB4wAgAUENRhsgAUEMRhsPC0EJQQtB4wAgAUEORhsgAUELRhsPC0EJQeMAIAFBC0YbDwtBAUHjACABQQ9GGw8LIAFBfmoiAUEETw0BIAFBAnQoAtCBwIAADwsgAUF6aiIBQQlPDQAgAUECdCgC4IHAgAAhAgsgAgsIAEGEgsCAAAsaACABIAAgAUEBIAFBAUsbQQAgARCTgICAAAvQAQECfyOAgICAAEEQayIFJICAgIAAIAUgA0EfcToADwJAAkBBICACQQRqIgJBASADdCIDIAIgA0sbIgNBf2pnayICQf//A3FBfWoiBkENTw0AIAFCASACrIanakF8aiAGQQJ0IgMoAsCCwIAANgIAIAMgATYCwILAgAAMAQsgAUIBQSAgA0GDgARqQRB2QX9qZ2siA61C//8Dg4anQRB0akF8aiADQf//A3FBAnQiAygC9ILAgAA2AgAgAyABNgL0gsCAAAsgBUEQaiSAgICAAAtnAgJ/AX4jgICAgABBEGsiASSAgICAAEEAIQIgAUEAOgAMAkBBAUUNAEEAKQOogcCAACIDpyAAQQEgAEEBSxtBAEEAIANCIIinKAIAEYCAgIAAgICAgAAhAgsgAUEQaiSAgICAACACC5UCAQR/I4CAgIAAQRBrIgQkgICAgAAgBCACQR9xOgAPQQAhBQJAQX8gAUEEaiIGIAYgAUkbIgFBASACdCICIAEgAksbIgFBf2pnIgJFDQACQAJAQSAgAmsiAkH//wNxQX1qIgZBDU8NAEIBIAKtQv//A4OGpyEHIAZBAnQiAigCwILAgAAiAUUNASACQcCCwIAAaiAHIAFqQXxqKAIANgIAIAEhBQwCCyABQYOABGpBEHYQloCAgAAhBQwBCyACQYyCwIAAaiEBAkAgAigCjILAgAAiAkH//wNxDQBBARCWgICAACICRQ0BIAEgAiAHajYCACACIQUMAQsgASACIAdqNgIAIAIhBQsgBEEQaiSAgICAACAFC2QBAn9CAUEgIABBf2pnayIArUL//wODhqchAQJAIABB//8DcUECdCICKAL0gsCAACIARQ0AIAJB9ILAgABqIAFBEHQgAGpBfGooAgA2AgAgAA8LQQAgAUAAIgBBEHQgAEF/RhsLHAAgAUEAIAQgBCACIAMgBCAEEJiAgIAAQQFxGwvgAQECfyOAgICAAEEQayIGJICAgIAAIAYgA0EfcToAD0F/IARBBGoiByAHIARJGyIEQQEgA3QiAyAEIANLGyEEAkACQAJAIAJBBGoiAiADIAIgA0sbIgNBf2pnIgJBb2pBDEsNACAEQX9qZyIDDQFBACEDDAILQgFBICADQYOABGpBEHZBf2pna61C//8Dg4anQgFBICAEQYOABGpBEHZBf2pna61C//8Dg4anRiEDDAELQgFBICACa61C//8Dg4anQgFBICADa61C//8Dg4anRiEDCyAGQRBqJICAgIAAIAMLC44CAQBBgIDAAAuEAnJlZnVzYWxfc3RhcnQAdGltZV90b2tlbl9saW1pdABib3VuZGFyeV9jb3JyZWN0AHVua25vd24AbW9kZWxfZW5kZWRfdHVybgBpbnZhbGlkX2VuZF9yZWFzb24AcHVzaF9mYWlsZWQAY3Jhc2hlZAA4ABAAEAAAAA4AEAAQAAAAaAAQAAcAAAAfABAAEAAAAAAAEAANAAAAXAAQAAsAAAAwABAABwAAAAAAAACwABAAAQAAAAIAAAADAAAABAAAAAIAAAAEAAAAAwAAAAMAAAADAAAAYwAAAAoAAAAKAAAABQAAAAYAAAAHAAAABAAAAAgAAAAJAAAAYwAAAGMAAAALAAAA"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +const MEM = W.memory; +const UTF8_IN = new TextEncoder(); +const UTF8_OUT = new TextDecoder(); +function str(v, held, at) { + if (typeof v !== "string") + throw new TypeError(`${at}: ${typeof v} is not a str`); + const bytes = UTF8_IN.encode(v); + const ptr = W.t27__alloc(bytes.length) >>> 0; + if (ptr === 0) + throw new RangeError(`${at}: no wasm memory for ${bytes.length} bytes`); + held.push(ptr, bytes.length); + new Uint8Array(MEM.buffer, ptr, bytes.length).set(bytes); + return [ptr, bytes.length]; +} +function strCall(call) { + const held = []; + try { + return call(held); + } finally { + for (let i = 0; i < held.length; i += 2) + W.t27__free(held[i], held[i + 1]); + } +} +function strOut() { + const at = W.t27__ret_at() >>> 0; + const slot = new DataView(MEM.buffer); + const ptr = slot.getUint32(at, true); + const len = slot.getUint32(at + 4, true); + return UTF8_OUT.decode(new Uint8Array(MEM.buffer, ptr, len)); +} +function nextState(state, event) { + return W.t27_next_state(int(state, 0, 4294967295, "next_state.state"), int(event, 0, 4294967295, "next_state.event")) >>> 0; +} +function releasesReadByRound(stored) { + return W.t27_releases_read_by_round(int(stored, 0, 4294967295, "releases_read_by_round.stored")) >>> 0; +} +function columnOf(claim, issueOpen, issueListKnown) { + return W.t27_column_of(int(claim, 0, 4294967295, "column_of.claim"), issueOpen ? 1 : 0, issueListKnown ? 1 : 0) >>> 0; +} +function chosenOf(e0, e1, e2, e3, e4) { + return W.t27_chosen_of(e0 ? 1 : 0, e1 ? 1 : 0, e2 ? 1 : 0, e3 ? 1 : 0, e4 ? 1 : 0) >>> 0; +} +function canStart(running, limit) { + return (W.t27_can_start(int(running, 0, 4294967295, "can_start.running"), int(limit, 0, 4294967295, "can_start.limit")) & 1) !== 0; +} +function reviewOf(total, judged, met, committedKnown, committed, priorSendBacks) { + return W.t27_review_of(int(total, 0, 4294967295, "review_of.total"), int(judged, 0, 4294967295, "review_of.judged"), int(met, 0, 4294967295, "review_of.met"), committedKnown ? 1 : 0, int(committed, 0, 4294967295, "review_of.committed"), int(priorSendBacks, 0, 4294967295, "review_of.prior_send_backs")) >>> 0; +} +function retryAllowed(realAttempts) { + return (W.t27_retry_allowed(int(realAttempts, 0, 4294967295, "retry_allowed.real_attempts")) & 1) !== 0; +} +function countsAgainstIssue(interrupted) { + return (W.t27_counts_against_issue(interrupted ? 1 : 0) & 1) !== 0; +} +function leaseTaken(expired, sameHolder) { + return (W.t27_lease_taken(expired ? 1 : 0, sameHolder ? 1 : 0) & 1) !== 0; +} +function ownersPerTerm() { + return W.t27_owners_per_term() >>> 0; +} +function stallReapable(ageMinutes) { + return (W.t27_stall_reapable(int(ageMinutes, 0, 4294967295, "stall_reapable.age_minutes")) & 1) !== 0; +} +function followupRounds(requestsDuringRound) { + return W.t27_followup_rounds(int(requestsDuringRound, 0, 4294967295, "followup_rounds.requests_during_round")) >>> 0; +} +function acceptRequiresPullRequest() { + return (W.t27_accept_requires_pull_request() & 1) !== 0; +} +function writerEnforcesTable() { + return (W.t27_writer_enforces_table() & 1) !== 0; +} +function isValidEndReason(reason) { + return (W.t27_is_valid_end_reason(int(reason, 0, 4294967295, "is_valid_end_reason.reason")) & 1) !== 0; +} +function endReasonToName(reason) { + W.t27_end_reason_to_name(int(reason, 0, 4294967295, "end_reason_to_name.reason")); + return strOut(); +} +function isNoDiffEndReason(reason) { + return (W.t27_is_no_diff_end_reason(int(reason, 0, 4294967295, "is_no_diff_end_reason.reason")) & 1) !== 0; +} +function isCrashEndReason(reason) { + return (W.t27_is_crash_end_reason(int(reason, 0, 4294967295, "is_crash_end_reason.reason")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze(["claim_of", "fence_after"]); +export { + __NOT_LOWERED__, + acceptRequiresPullRequest, + canStart, + chosenOf, + columnOf, + countsAgainstIssue, + endReasonToName, + followupRounds, + isCrashEndReason, + isNoDiffEndReason, + isValidEndReason, + leaseTaken, + nextState, + ownersPerTerm, + releasesReadByRound, + retryAllowed, + reviewOf, + stallReapable, + writerEnforcesTable +}; diff --git a/apps/website/public/widgets/hive-column/logic.t27 b/apps/website/public/widgets/hive-column/logic.t27 new file mode 100644 index 0000000000..42911bfbb6 --- /dev/null +++ b/apps/website/public/widgets/hive-column/logic.t27 @@ -0,0 +1,594 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done +; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written +; down in this family of repositories and only one of them moves work: the round of the trios supervisor +; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it, +; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with +; the other cycles named as adapters and their gaps stated. Every decision below is a function the C +; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py): +; ts its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the +; public board's column, the single-flight round gate +; queend its Swift policy binary, built at the pin and asked choose, review and retry +; pg PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch +; writers on real rows, the criteria release, the CI take-back, the round's own SELECT +; (17.7 at this pin; the run at the old pin c25e1b02 used 16.15) +; lotus gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which +; this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin +; c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host) +; live one snapshot of the public endpoints of the deployed supervisor +; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch +; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read +; from the source and named below: runner lanes (a claim in state queued that takes no worker slot, +; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees) +; and the public card's verdict and judgedHead. +; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a +; revision other than the pin. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenDispatch; + +pub const KIND : str = "queen-dispatch"; +pub const ID : str = "queen/dispatch"; +pub const NAME : str = "The Queen's task lifecycle, from candidate to done"; +pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS"; +pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size"; +pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef"; +pub const CONSUMER_REPO : str = "gHashTag/trinity"; +pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e"; +pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530"; +pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149"; +; --- The decision: one canonical machine, the others are adapters --------------------------------- +pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)"; +pub const ADAPTERS : [5]str = [ + "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend", + "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design", + "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing", + "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task", + "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here" +]; +pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state"; +pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"]; +; --- States of a dispatch row, and the events that move it ---------------------------------------- +pub const S_NONE : u32 = 0; +pub const S_RUNNING : u32 = 1; +pub const S_REFUSED : u32 = 2; +pub const S_ENDED : u32 = 3; +pub const S_WAIT : u32 = 4; +pub const S_ACCEPT : u32 = 5; +pub const S_SENDBACK : u32 = 6; +pub const S_ESCALATE : u32 = 7; +pub const S_EMPTY : u32 = 8; +pub const S_STALE : u32 = 9; +pub const S_REAPED : u32 = 10; +pub const S_RELEASED : u32 = 11; +pub const ILLEGAL : u32 = 99; +pub const E_START : u32 = 0; +pub const E_REFUSE : u32 = 1; +pub const E_END : u32 = 2; +pub const E_END_OTHER_TURN : u32 = 3; +pub const E_REAP_STALL : u32 = 4; +pub const E_REAP_BOOT : u32 = 5; +pub const E_VERDICT_ACCEPT : u32 = 6; +pub const E_VERDICT_SENDBACK : u32 = 7; +pub const E_VERDICT_ESCALATE : u32 = 8; +pub const E_VERDICT_WAIT : u32 = 9; +pub const E_VERDICT_EMPTY : u32 = 10; +pub const E_CRITERIA_CHANGED : u32 = 11; +pub const E_CI_RED_UNDER_CEILING : u32 = 12; +pub const E_CI_RED_AT_CEILING : u32 = 13; +pub const E_FLOOR_PASSED : u32 = 14; +pub const E_REDISPATCH : u32 = 15; +pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)"; +pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation"; +pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it"; +; --- Claims: what a row costs its issue ------------------------------------------------------------ +pub const V_NONE : u32 = 0; +pub const V_BLANK : u32 = 1; +pub const V_WAIT : u32 = 2; +pub const V_EMPTY : u32 = 3; +pub const V_SENDBACK : u32 = 4; +pub const V_ESCALATE : u32 = 5; +pub const V_ACCEPT : u32 = 6; +pub const V_FAILED : u32 = 7; +pub const V_CANCELLED : u32 = 8; +pub const V_STALE : u32 = 9; +pub const V_OTHER : u32 = 10; +pub const C_RUNNING : u32 = 0; +pub const C_ACCEPTED : u32 = 1; +pub const C_REJECTED : u32 = 2; +pub const C_AWAITING_REVIEW : u32 = 3; +pub const C_FAILED : u32 = 4; +pub const EMPTY_FLOOR_MS : u64 = 1800000; +pub const SENDBACK_FLOOR_MS : u64 = 3600000; +pub const WAIT_FLOOR_MS : u64 = 21600000; +pub const CEILING_RELEASE_MS : u64 = 3600000; +pub const MAX_CEILING_RELEASES : u32 = 1; +pub const SENDBACK_CEILING : u32 = 2; +pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it"; +; --- Choosing ---------------------------------------------------------------------------------------- +pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates"; +pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies"; +pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing"; +pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only"; +pub const DEFAULT_WORKER_LIMIT : u32 = 4; +pub const NONE_CHOSEN : u32 = 5; +pub const COL_BACKLOG : u32 = 0; +pub const COL_BLOCKED : u32 = 1; +pub const COL_RUNNING : u32 = 2; +pub const COL_REVIEW : u32 = 3; +pub const COL_DONE : u32 = 4; +pub const COL_DROPPED : u32 = 5; +; --- Leases ------------------------------------------------------------------------------------------ +pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced"; +pub const LEASE_TTL_SECONDS : u32 = 180; +pub const LEASE_HEARTBEAT_SECONDS : u32 = 60; +pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane"; +pub const STALL_MINUTES : u32 = 120; +; --- Review, retries, evidence, cancellation -------------------------------------------------------- +pub const R_ACCEPT : u32 = 0; +pub const R_SENDBACK : u32 = 1; +pub const R_ESCALATE : u32 = 2; +pub const R_WAIT : u32 = 3; +pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)"; +pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container"; +pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists"; +pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict"; +pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped"; +; --- Events, persistence, idempotency, projections -------------------------------------------------- +pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)"; +pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round"; +pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"]; +pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board"; + +; --- End reasons for runs that produce no commits ----------------------------------------------------------- +; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted. +; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days. +pub const R_MODEL_ENDED_TURN : u32 = 0; // Model ended its turn without editing any files +pub const R_TIME_TOKEN_LIMIT : u32 = 1; // Hit time or token limit during execution +pub const R_CRASHED : u32 = 2; // Container or tool crashed (with exit code) +pub const R_BOUNDARY_CORRECT : u32 = 3; // Boundary file was already correct, no changes needed +pub const R_REFUSAL_START : u32 = 4; // Refusal before the run started (invalid task, missing resources) +pub const R_PUSH_FAILED : u32 = 5; // Push to remote failed after successful run +pub const R_UNKNOWN : u32 = 6; // Cause could not be determined by the runner + +pub const END_REASON_COUNT : u32 = 7; +pub const END_REASON_NAMES : [7]str = [ + "model_ended_turn", + "time_token_limit", + "crashed", + "boundary_correct", + "refusal_start", + "push_failed", + "unknown" +]; + +; --- Public board extension: show end reason on empty cards ----------------------------------------------- +; The public board's "empty" cards now include the end reason when available, enabling breakdown of +; the 56.0% empty-branch share by reason from public endpoints alone. +pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available"; +pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints"; +; --- Agents and workers --------------------------------------------------------------------------------- +pub const AGENT_LETTERS : u32 = 27; +pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27"; +; --- The specs this one sits beside ---------------------------------------------------------------------- +pub const NEIGHBOURS : [6]str = [ + "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master", + "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort", + "specs/queen/brain_summaries.t27: does not compile in C at master", + "specs/brain/brain.t27: no function; does not compile in C at master", + "specs/brain/bus.t27: compiles, one test of a version constant", + "specs/brain/cognitive_loop.t27: compiles, one test of a version constant" +]; +; --- Findings: measured, the tag names the evidence ------------------------------------------------------ +pub const FINDINGS_COUNT : u32 = 15; +pub const FINDINGS : [15]str = [ + "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]", + "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]", + "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]", + "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]", + "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]", + "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]", + "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]", + "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]", + "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]", + "f59 nothing cancels a running bee [source]", + "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]", + "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]", + "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]", + "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]", + "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]" +]; +pub const ENABLED : bool = true; + +; --- Rules ------------------------------------------------------------------------------------------------------ + +pub fn next_state(state: u32, event: u32) -> u32 { + if (state == S_NONE) { + if (event == E_START) { return S_RUNNING; } + if (event == E_REFUSE) { return S_REFUSED; } + return ILLEGAL; + } + if (state == S_RUNNING) { + if (event == E_END) { return S_ENDED; } + if (event == E_REAP_STALL) { return S_REAPED; } + if (event == E_REAP_BOOT) { return S_REAPED; } + return ILLEGAL; + } + if (state == S_ENDED or state == S_WAIT) { + if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; } + if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; } + if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; } + if (event == E_VERDICT_WAIT) { return S_WAIT; } + if (event == E_VERDICT_EMPTY) { return S_EMPTY; } + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + if (event == E_FLOOR_PASSED) { return S_RELEASED; } + return ILLEGAL; + } + if (state == S_ACCEPT) { + if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; } + if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; } + return ILLEGAL; + } + if (state == S_SENDBACK or state == S_EMPTY) { + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + if (event == E_FLOOR_PASSED) { return S_RELEASED; } + return ILLEGAL; + } + if (state == S_ESCALATE) { + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + return ILLEGAL; + } + if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) { + if (event == E_REDISPATCH) { return S_RUNNING; } + return ILLEGAL; + } + return ILLEGAL; +} + +pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 { + if (finished == false) { return C_RUNNING; } + if (verdict == V_ACCEPT) { return C_ACCEPTED; } + if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; } + if (verdict == V_EMPTY) { + if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; } + return C_REJECTED; + } + if (verdict == V_STALE) { return C_FAILED; } + if (verdict == V_SENDBACK) { + if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; } + if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; } + if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; } + return C_REJECTED; + } + if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) { + if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; } + } + return C_AWAITING_REVIEW; +} + +; What the round and the board actually pass for `releases`: neither selects the column. +pub fn releases_read_by_round(stored: u32) -> u32 { + return 0; +} + +pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 { + if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; } + if (claim == C_RUNNING) { return COL_RUNNING; } + if (claim == C_ACCEPTED) { return COL_DONE; } + if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; } + return COL_DROPPED; +} + +pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 { + if (e0) { return 0; } + if (e1) { return 1; } + if (e2) { return 2; } + if (e3) { return 3; } + if (e4) { return 4; } + return NONE_CHOSEN; +} + +pub fn can_start(running: u32, limit: u32) -> bool { + return running < limit; +} + +pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 { + if (total == 0) { return R_ESCALATE; } + if (judged < total) { return R_WAIT; } + if (met == total) { + if (committed_known and committed > 0) { return R_ACCEPT; } + return R_ESCALATE; + } + if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; } + return R_SENDBACK; +} + +pub fn retry_allowed(real_attempts: u32) -> bool { + return real_attempts < SENDBACK_CEILING; +} + +pub fn counts_against_issue(interrupted: bool) -> bool { + return interrupted == false; +} + +pub fn lease_taken(expired: bool, same_holder: bool) -> bool { + return expired or same_holder; +} + +pub fn fence_after(fence: u64) -> u64 { + return fence + 1; +} + +pub fn owners_per_term() -> u32 { + return 1; +} + +pub fn stall_reapable(age_minutes: u32) -> bool { + return age_minutes > STALL_MINUTES; +} + +pub fn followup_rounds(requests_during_round: u32) -> u32 { + if (requests_during_round > 0) { return 1; } + return 0; +} + +pub fn accept_requires_pull_request() -> bool { + return false; +} + +pub fn writer_enforces_table() -> bool { + return false; +} + +invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; } +invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; } +invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; } +invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; } +invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); } +invariant all_end_reasons_valid { + assert is_valid_end_reason(R_MODEL_ENDED_TURN); + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT); + assert is_valid_end_reason(R_CRASHED); + assert is_valid_end_reason(R_BOUNDARY_CORRECT); + assert is_valid_end_reason(R_REFUSAL_START); + assert is_valid_end_reason(R_PUSH_FAILED); + assert is_valid_end_reason(R_UNKNOWN); +} + +; --- End reason handling functions -------------------------------------------------------------------------- + +pub fn is_valid_end_reason(reason: u32) -> bool { + return reason < END_REASON_COUNT; +} + +pub fn end_reason_to_name(reason: u32) -> str { + if (is_valid_end_reason(reason)) { + return END_REASON_NAMES[reason]; + } + return "invalid_end_reason"; +} + +pub fn is_no_diff_end_reason(reason: u32) -> bool { + return reason == R_MODEL_ENDED_TURN + or reason == R_TIME_TOKEN_LIMIT + or reason == R_BOUNDARY_CORRECT + or reason == R_REFUSAL_START + or reason == R_PUSH_FAILED + or reason == R_UNKNOWN; +} + +pub fn is_crash_end_reason(reason: u32) -> bool { + return reason == R_CRASHED; +} + +test the_table_moves_only_along_its_guards { + assert next_state(S_NONE, E_START) == S_RUNNING; + assert next_state(S_RUNNING, E_END) == S_ENDED; + assert next_state(S_ENDED, E_END) == ILLEGAL; + assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL; + assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL; + assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE; + assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL; + assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL; + assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING; + assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL; + // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886). + assert next_state(S_NONE, E_REFUSE) == S_REFUSED; + assert next_state(S_NONE, E_END) == ILLEGAL; + assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED; + assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED; + assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK; + assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE; + assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT; + assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY; + assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK; + assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_START) == ILLEGAL; + assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING; +} + +test a_claim_follows_the_verdict_and_three_clocks { + assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING; + assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED; + assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED; + assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED; + assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED; + assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW; + // #9886: each verdict that frees the issue, the send-back floor at its edge with a release + // already spent, and the wait floor's two edges. + assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED; + assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW; + assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW; +} + +test a_closed_issue_is_drawn_done_whatever_its_verdict { + assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE; + assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW; + assert column_of(C_RUNNING, false, true) == COL_RUNNING; + assert column_of(C_FAILED, true, true) == COL_DROPPED; + assert column_of(C_ACCEPTED, true, true) == COL_DONE; +} + +test the_first_eligible_wins_in_any_order { + assert chosen_of(false, false, true, true, false) == 2; + assert chosen_of(false, true, false, false, true) == 1; + assert chosen_of(false, false, false, false, false) == NONE_CHOSEN; + assert chosen_of(true, true, false, false, false) == 0; + assert chosen_of(false, false, false, true, true) == 3; + assert chosen_of(false, false, false, false, true) == 4; + assert can_start(3, DEFAULT_WORKER_LIMIT) == true; + assert can_start(4, DEFAULT_WORKER_LIMIT) == false; +} + +test review_escalates_without_work_and_after_two_send_backs { + assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE; + assert review_of(2, 1, 1, true, 1, 0) == R_WAIT; + assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT; + assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE; + assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE; + assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK; + assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE; + assert retry_allowed(1) == true; + assert retry_allowed(2) == false; + assert counts_against_issue(true) == false; + assert counts_against_issue(false) == true; +} + +test one_lease_one_owner_and_age_alone_reaps { + assert lease_taken(false, false) == false; + assert lease_taken(true, false) == true; + assert lease_taken(false, true) == true; + assert fence_after(3) == 4; + assert owners_per_term() == 1; + assert stall_reapable(121) == true; + assert stall_reapable(119) == false; + // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself. + assert stall_reapable(120) == false; + assert followup_rounds(5) == 1; + assert followup_rounds(0) == 0; + assert accept_requires_pull_request() == false; + assert writer_enforces_table() == false; +} + +; --- End reason tests -------------------------------------------------------------------------------------- + +test end_reason_constants_are_correct { + assert R_MODEL_ENDED_TURN == 0; + assert R_TIME_TOKEN_LIMIT == 1; + assert R_CRASHED == 2; + assert R_BOUNDARY_CORRECT == 3; + assert R_REFUSAL_START == 4; + assert R_PUSH_FAILED == 5; + assert R_UNKNOWN == 6; + assert END_REASON_COUNT == 7; + // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two + // call the functions that read the constants: the last reason is named, one past it is not. + assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown"; + assert is_valid_end_reason(END_REASON_COUNT) == false; +} + +test end_reason_names_array_matches_constants { + assert END_REASON_NAMES[0] == "model_ended_turn"; + assert END_REASON_NAMES[1] == "time_token_limit"; + assert END_REASON_NAMES[2] == "crashed"; + assert END_REASON_NAMES[3] == "boundary_correct"; + assert END_REASON_NAMES[4] == "refusal_start"; + assert END_REASON_NAMES[5] == "push_failed"; + assert END_REASON_NAMES[6] == "unknown"; +} + +test end_reason_validation_works { + assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_valid_end_reason(R_CRASHED) == true; + assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_valid_end_reason(R_REFUSAL_START) == true; + assert is_valid_end_reason(R_PUSH_FAILED) == true; + assert is_valid_end_reason(R_UNKNOWN) == true; + assert is_valid_end_reason(7) == false; // One past the last valid reason + assert is_valid_end_reason(99) == false; // Way past the last valid reason +} + +test end_reason_to_name_conversion { + assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn"; + assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit"; + assert end_reason_to_name(R_CRASHED) == "crashed"; + assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct"; + assert end_reason_to_name(R_REFUSAL_START) == "refusal_start"; + assert end_reason_to_name(R_PUSH_FAILED) == "push_failed"; + assert end_reason_to_name(R_UNKNOWN) == "unknown"; + assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string +} + +test no_diff_end_reason_classification { + assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_no_diff_end_reason(R_REFUSAL_START) == true; + assert is_no_diff_end_reason(R_PUSH_FAILED) == true; + assert is_no_diff_end_reason(R_UNKNOWN) == true; + assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category +} + +test crash_end_reason_classification { + assert is_crash_end_reason(R_CRASHED) == true; + assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false; + assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false; + assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false; + assert is_crash_end_reason(R_REFUSAL_START) == false; + assert is_crash_end_reason(R_PUSH_FAILED) == false; + assert is_crash_end_reason(R_UNKNOWN) == false; +} + +test end_reason_enables_breakdown_of_empty_branches { + // This test validates that the end reason vocabulary enables breaking down + // the 56.0% empty-branch share by reason from public endpoints alone + + // Simulate counts for empty branches by reason + var model_ended_count : u32 = 100; + var token_limit_count : u32 = 80; + var boundary_correct_count : u32 = 60; + var refusal_start_count : u32 = 40; + var push_failed_count : u32 = 30; + var unknown_count : u32 = 7; // Should be < 10% of total + var crash_count : u32 = 0; + + var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count + crash_count; + + // Verify unknown is less than 10% of total (success criterion from issue) + assert unknown_count < total_empty / 10; + + // Verify we can calculate breakdown percentages + var model_percentage : u32 = (model_ended_count * 100) / total_empty; + var token_percentage : u32 = (token_limit_count * 100) / total_empty; + var unknown_percentage : u32 = (unknown_count * 100) / total_empty; + + assert model_percentage > 0; + assert token_percentage > 0; + assert unknown_percentage < 10; // Unknown should be < 10% + + // Verify no-diff vs crash categorization works + var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count; + var crash_total : u32 = crash_count; + + assert no_diff_total == total_empty - crash_total; +} diff --git a/apps/website/public/widgets/hive-column/tool.js b/apps/website/public/widgets/hive-column/tool.js new file mode 100644 index 0000000000..9ba83f72bb --- /dev/null +++ b/apps/website/public/widgets/hive-column/tool.js @@ -0,0 +1,25 @@ +// hive-column/tool.js -- lesson 26 of queen-and-hive: which column a task's card is in on the public +// board. Every card's column is column_of(claim, issue open, list known), the fn body of gHashTag/t27 +// specs/queen/dispatch.t27 compiled to wasm (./logic.js). Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-column.t27. +import { columnOf } from './logic.js' +import { K, W, ask, board, controls, el, fill, head, root, say, source } from '../hive-kit.js' + +const NC = K('K_CLAIMS', 5), NCOL = K('K_COLUMNS', 6) +const ctl = controls([ + { key: 'open', label: say('SAY_OPEN'), kind: 'toggle' }, + { key: 'known', label: say('SAY_KNOWN'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { open: K('K_PRESET_OPEN', []), known: K('K_PRESET_KNOWN', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const COL_TONES = [2, 6, 1, 4, 0, 8] +const CLAIM_TONES = [1, 0, 4, 2, 6] + +function draw(s) { + const cards = [...Array(NC)].map((_, c) => ({ col: columnOf(c, !!s.open, !!s.known), title: say('SAY_CLAIM_NAMES', c), sub: say('SAY_CLAIM'), tone: CLAIM_TONES[c] })) + const done = cards.filter((c) => c.col === K('K_COL_DONE', 4)).length + out.replaceChildren(head(fill(say('SAY_IN_DONE'), done, cards.length)), board([...Array(NCOL)].map((_, i) => ({ name: say('SAY_COLUMN_NAMES', i), tone: COL_TONES[i] })), cards)) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-cooldown/card.png b/apps/website/public/widgets/hive-cooldown/card.png new file mode 100644 index 0000000000..33a58add54 Binary files /dev/null and b/apps/website/public/widgets/hive-cooldown/card.png differ diff --git a/apps/website/public/widgets/hive-cooldown/hive-cooldown.t27 b/apps/website/public/widgets/hive-cooldown/hive-cooldown.t27 new file mode 100644 index 0000000000..bef00a30c0 --- /dev/null +++ b/apps/website/public/widgets/hive-cooldown/hive-cooldown.t27 @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-cooldown.t27 -- The cooldown clock, /stop and /mute +; Source of truth for public/widgets/hive-cooldown/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-cooldown/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 24 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the milliseconds since the last bot message, the minutes since +; /stop and since /mute, and whether the agent was tagged. The dial is cooled(ms); the cards are +; may_answer_after_stop(from a bot or a person, ms) and heard_while_muted(tagged, ms). + +; WHAT IT DOES NOT CLAIM: it sends no command: the minutes are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_cooldown; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-cooldown"; +pub const TITLE : str = "The cooldown clock, /stop and /mute"; +pub const DESCRIPTION : str = "One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard."; +pub const IMAGE_ALT : str = "Cooldown card: 3200 ms since the last bot message, the dial filling to 3.2 of 10 s with a gold mark at 5 s: not cooled, the bot waits. A /stop 4 minutes ago: bots stay silent, a person is heard. A /mute 20 minutes ago: only a tag by name gets through."; +pub const HOOK : str = "A stop cuts bot-to-bot turns only, a mute still answers its own tag: neither one can lose a person or a direct call."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is cooled, may_answer_after_stop and heard_while_muted, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 24 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MS_MAX : u16 = 10000; +pub const K_MS_STEP : u8 = 100; +pub const K_BOT_REPLY_COOLDOWN_MS : u32 = 5000; +pub const K_STOP_WINDOW_MS : u32 = 600000; +pub const K_MUTE_MS : u32 = 3600000; +pub const K_MS_PER_MINUTE : u32 = 60000; +pub const K_STOP_MAX : u8 = 20; +pub const K_MUTE_MAX : u8 = 90; +pub const K_PRESET_MS : [4]u16 = [3200, 5000, 8000, 1000]; +pub const K_PRESET_STOP : [4]u8 = [4, 12, 4, 20]; +pub const K_PRESET_MUTE : [4]u8 = [20, 20, 70, 5]; +pub const K_PRESET_TAGGED : [4]u8 = [0, 0, 0, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["3.2 s, stop 4 min ago", "Exactly 5 s", "Mute ran out", "Tagged while muted"]; +pub const SAY_ASK : str = "Agents answer fast. How does the hive keep them from flooding a chat?"; +pub const SAY_MS : str = "ms since the last bot message"; +pub const SAY_STOP : str = "minutes since /stop"; +pub const SAY_MUTE : str = "minutes since /mute"; +pub const SAY_TAGGED : str = "the agent is tagged by name"; +pub const SAY_COOLED : str = "cooled: a bot may answer"; +pub const SAY_WAIT : str = "not cooled: the bot waits"; +pub const SAY_DIAL : str = "of 10 s"; +pub const SAY_STOP_CARDS : [2]str = ["a bot after /stop", "a person after /stop"]; +pub const SAY_MUTE_CARD : str = "the muted agent"; +pub const SAY_HEARD : str = "heard"; +pub const SAY_SILENT : str = "silent"; +pub const SAY_NOTE : str = "/stop lasts 10 minutes and /mute an hour; both are clocks, so the hive needs no one to switch it back on."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_clocks_are_the_specs { + assert K_BOT_REPLY_COOLDOWN_MS == 5000; + assert K_STOP_WINDOW_MS == K_MS_PER_MINUTE * 10; + assert K_MUTE_MS == K_MS_PER_MINUTE * 60; +} + +test the_sliders_cross_each_window { + assert K_STOP_MAX * K_MS_PER_MINUTE > K_STOP_WINDOW_MS; + assert K_MUTE_MAX * K_MS_PER_MINUTE > K_MUTE_MS; + assert K_MS_MAX > K_BOT_REPLY_COOLDOWN_MS; +} diff --git a/apps/website/public/widgets/hive-cooldown/index.html b/apps/website/public/widgets/hive-cooldown/index.html new file mode 100644 index 0000000000..87bbb13764 --- /dev/null +++ b/apps/website/public/widgets/hive-cooldown/index.html @@ -0,0 +1,47 @@ + + + + + + +The cooldown clock, /stop and /mute · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The cooldown clock, /stop and /mute

+

One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is cooled, may_answer_after_stop and heard_while_muted, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 24 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-cooldown/logic.js b/apps/website/public/widgets/hive-cooldown/logic.js new file mode 100644 index 0000000000..51d160eee0 --- /dev/null +++ b/apps/website/public/widgets/hive-cooldown/logic.js @@ -0,0 +1,116 @@ +// Generated by scripts/t27-logic.mjs from agent-hive-group.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 9cdd5ed3872b60e900a364d843f908920334408f965bde194d233b64534076f9 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-cooldown/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJAVgAX8Bf2ACf38Bf2ADf39/AX9gBH9/f38Bf2AFf39/f38BfwMZGAABAAACAAIBAAEAAQEAAQADAAAAAgAABAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB74DGQZtZW1vcnkCABR0MjdfZG1faGFuZG9mZnNfc2VudAAADXQyN19kbV90YWdfb2sAAQ50MjdfZG1fY2FycmllcwACEHQyN19kbV9ydW5zX3R1cm4AAwx0MjdfZG1faGVhcmQABBF0Mjdfc3VtbWFyeV9jb3VudAAFEnQyN19yZWNyZWF0ZV90b3BpYwAGEXQyN19tYXlfc2V0X2JyaWVmAAcOdDI3X2JyaWVmX2ZpdHMACBB0MjdfYnJpZWZfc291cmNlAAkTdDI3X2tlZXBzX3NlZW5fbWFyawAKC3QyN190dXJuX29rAAsVdDI3X2hlYXJkX3doaWxlX211dGVkAAwJdDI3X211dGVkAA0ZdDI3X21heV9hbnN3ZXJfYWZ0ZXJfc3RvcAAOC3QyN19zdG9wcGVkAA8OdDI3X2ZvbGxvd3NfdXAAEBF0Mjdfc3BlYWtzX29uX2FsbAARDnQyN190eXBpbmdfZHVlABIPdDI3X2NsYWltX2ZyZXNoABMOdDI3X21heV9hbnN3ZXIAFAp0MjdfY29vbGVkABUOdDI3X2NoYWluX29wZW4AFg10MjdfYWRkcmVzc2VkABcK7AIYDAAgAEECIABBAkkbCyoBAX9BASECAkACQAJAIABBf2oOAgIAAQsgAUHA7u0ASQ8LQQAhAgsgAgsHACAAQQJGCwcAIABBAUYLDQAgAEEBcyABIAJxcgsTACAAQcgBIABByAFJG0EyIAAbCxIAIAAgAXFBf3MgAkG/zyRLcQsKACAAIAFBAXNxCwgAIABB6QdJCw8AQQIgAUEBcSAAQQFxGwsEACAACwQAIAALDQAgACABQf/c2wFLcgsKACAAQYDd2wFJCw8AIABBAXMgAUG/zyRLcgsJACAAQcDPJEkLFQAgAEF/cyABcSACcSADQcCpB0lxCwUAIABFCwgAIABBnx9LCwkAIABBoPc2SQsjAQF/QQEhAwJAIABBAXFFDQAgAUEDSSACQYcnS3EhAwsgAwsIACAAQYcnSwsHACAAQQNJCygBAX9BASEFAkAgAEEBcQ0AIAJBAXENACAEQQFzIAEgA3JxIQULIAUL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function addressed(tagged, allTag, replyToMe, ownTopic, fromBot) { + return (W.t27_addressed(tagged ? 1 : 0, allTag ? 1 : 0, replyToMe ? 1 : 0, ownTopic ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function chainOpen(depth) { + return (W.t27_chain_open(int(depth, 0, 4294967295, "chain_open.depth")) & 1) !== 0; +} +function cooled(sinceLastMs) { + return (W.t27_cooled(int(sinceLastMs, 0, 4294967295, "cooled.since_last_ms")) & 1) !== 0; +} +function mayAnswer(fromBot, depth, sinceLastMs) { + return (W.t27_may_answer(fromBot ? 1 : 0, int(depth, 0, 4294967295, "may_answer.depth"), int(sinceLastMs, 0, 4294967295, "may_answer.since_last_ms")) & 1) !== 0; +} +function claimFresh(ageMs) { + return (W.t27_claim_fresh(int(ageMs, 0, 4294967295, "claim_fresh.age_ms")) & 1) !== 0; +} +function typingDue(sinceLastMs) { + return (W.t27_typing_due(int(sinceLastMs, 0, 4294967295, "typing_due.since_last_ms")) & 1) !== 0; +} +function speaksOnAll(rank) { + return (W.t27_speaks_on_all(int(rank, 0, 4294967295, "speaks_on_all.rank")) & 1) !== 0; +} +function followsUp(fromBot, samePerson, sameThread, ageMs) { + return (W.t27_follows_up(fromBot ? 1 : 0, samePerson ? 1 : 0, sameThread ? 1 : 0, int(ageMs, 0, 4294967295, "follows_up.age_ms")) & 1) !== 0; +} +function stopped(sinceStopMs) { + return (W.t27_stopped(int(sinceStopMs, 0, 4294967295, "stopped.since_stop_ms")) & 1) !== 0; +} +function mayAnswerAfterStop(fromBot, sinceStopMs) { + return (W.t27_may_answer_after_stop(fromBot ? 1 : 0, int(sinceStopMs, 0, 4294967295, "may_answer_after_stop.since_stop_ms")) & 1) !== 0; +} +function muted(sinceMuteMs) { + return (W.t27_muted(int(sinceMuteMs, 0, 4294967295, "muted.since_mute_ms")) & 1) !== 0; +} +function heardWhileMuted(tagged, sinceMuteMs) { + return (W.t27_heard_while_muted(tagged ? 1 : 0, int(sinceMuteMs, 0, 4294967295, "heard_while_muted.since_mute_ms")) & 1) !== 0; +} +function turnOk(answerSent, reactionOk) { + return (W.t27_turn_ok(answerSent ? 1 : 0, reactionOk ? 1 : 0) & 1) !== 0; +} +function keepsSeenMark(answerSent) { + return (W.t27_keeps_seen_mark(answerSent ? 1 : 0) & 1) !== 0; +} +function briefSource(hasTopicBrief, hasGroupBrief) { + return W.t27_brief_source(hasTopicBrief ? 1 : 0, hasGroupBrief ? 1 : 0) & 255; +} +function briefFits(chars) { + return (W.t27_brief_fits(int(chars, 0, 4294967295, "brief_fits.chars")) & 1) !== 0; +} +function maySetBrief(isAdmin, fromBot) { + return (W.t27_may_set_brief(isAdmin ? 1 : 0, fromBot ? 1 : 0) & 1) !== 0; +} +function recreateTopic(stored, threadFound, sinceLastTryMs) { + return (W.t27_recreate_topic(stored ? 1 : 0, threadFound ? 1 : 0, int(sinceLastTryMs, 0, 4294967295, "recreate_topic.since_last_try_ms")) & 1) !== 0; +} +function summaryCount(asked) { + return W.t27_summary_count(int(asked, 0, 4294967295, "summary_count.asked")) >>> 0; +} +function dmHeard(fromBot, colleague, tagOk) { + return (W.t27_dm_heard(fromBot ? 1 : 0, colleague ? 1 : 0, tagOk ? 1 : 0) & 1) !== 0; +} +function dmRunsTurn(kind) { + return (W.t27_dm_runs_turn(int(kind, 0, 255, "dm_runs_turn.kind")) & 1) !== 0; +} +function dmCarries(kind) { + return (W.t27_dm_carries(int(kind, 0, 255, "dm_carries.kind")) & 1) !== 0; +} +function dmTagOk(kind, sinceAskMs) { + return (W.t27_dm_tag_ok(int(kind, 0, 255, "dm_tag_ok.kind"), int(sinceAskMs, 0, 4294967295, "dm_tag_ok.since_ask_ms")) & 1) !== 0; +} +function dmHandoffsSent(asked) { + return W.t27_dm_handoffs_sent(int(asked, 0, 4294967295, "dm_handoffs_sent.asked")) >>> 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + addressed, + briefFits, + briefSource, + chainOpen, + claimFresh, + cooled, + dmCarries, + dmHandoffsSent, + dmHeard, + dmRunsTurn, + dmTagOk, + followsUp, + heardWhileMuted, + keepsSeenMark, + mayAnswer, + mayAnswerAfterStop, + maySetBrief, + muted, + recreateTopic, + speaksOnAll, + stopped, + summaryCount, + turnOk, + typingDue +}; diff --git a/apps/website/public/widgets/hive-cooldown/logic.t27 b/apps/website/public/widgets/hive-cooldown/logic.t27 new file mode 100644 index 0000000000..2f801b0db7 --- /dev/null +++ b/apps/website/public/widgets/hive-cooldown/logic.t27 @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group. +// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots). +// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group". +// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/. +// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode +// switched on in @BotFather for every bot, which no API call can do. +// Telegram gives no loop protection between bots, so the depth and the cooldown +// below are the only thing that stops two agents from answering each other forever. +// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window. +// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept +// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute. +// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs, +// a topic a person deleted, and /summary. +// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private +// message to another bot by @username when Bot-to-Bot Communication Mode is on for both +// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the +// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn, +// and only while its ask is fresh. +// Earlier laws are unchanged; each version only adds, so a host on an older one stays true. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::agent_hive_group { + pub const KIND : str = "automation"; + pub const ID : str = "agent-hive-group"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 4; + + // Bot messages in a row after one human message: human -> A -> B -> C, then silence. + pub const MAX_BOT_DEPTH : u32 = 3; + // One agent answers another agent at most this often. + pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000; + // How long "create the bot for agent X" waits for Telegram's creation message. + pub const CLAIM_TTL_MS : u32 = 900000; + // The tag that calls every agent of the hive at once. + pub const ALL_TAG : str = "@all"; + // Suggested username of an agent's bot: t27__bot. + pub const BOT_USERNAME_PREFIX : str = "t27_"; + pub const BOT_USERNAME_SUFFIX : str = "_bot"; + + // A person may call an agent by its tag, by the all tag, by replying to it, + // or by writing in the agent's own topic. Another bot only by tag or reply: + // a bot never triggers the whole hive, and never wakes a topic's owner by + // merely writing there. + fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool { + return tagged || reply_to_me || (!from_bot && (all_tag || own_topic)); + } + + // depth = how many bot messages already stand between this one and the person. + fn chain_open(depth: u32) -> bool { + return depth < MAX_BOT_DEPTH; + } + + fn cooled(since_last_ms: u32) -> bool { + return since_last_ms >= BOT_REPLY_COOLDOWN_MS; + } + + // A person is always answered; a bot only inside the chain and after the cooldown. + fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool { + return !from_bot || (chain_open(depth) && cooled(since_last_ms)); + } + + fn claim_fresh(age_ms: u32) -> bool { + return age_ms < CLAIM_TTL_MS; + } + + // ---- v2 ---- + + // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute. + // The action is sent again this long after the last one, until the answer goes out. + pub const TYPING_REFRESH_MS : u32 = 4000; + // On the all tag only this many agents answer at once, best fit first; the rest + // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team. + pub const ALL_FIRST_SPEAKERS : u32 = 1; + // A person's next message in the same topic goes to the agent that answered + // last, without a new tag, for this long. + pub const FOLLOW_UP_WINDOW_MS : u32 = 120000; + // /stop: no bot answers a bot in that chat for this long. People are still answered. + pub const STOP_WINDOW_MS : u32 = 600000; + // /mute: the agent answers in that topic only when tagged by name, for this long. + pub const MUTE_MS : u32 = 3600000; + + fn typing_due(since_last_ms: u32) -> bool { + return since_last_ms >= TYPING_REFRESH_MS; + } + + // rank = the agent's place in the best-fit order for this message, from 0. + fn speaks_on_all(rank: u32) -> bool { + return rank < ALL_FIRST_SPEAKERS; + } + + // A bot never follows up: it must tag or reply, as v1 says. + fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool { + return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS; + } + + fn stopped(since_stop_ms: u32) -> bool { + return since_stop_ms < STOP_WINDOW_MS; + } + + // A stop cuts bot-to-bot turns only; a person is always heard. + fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool { + return !from_bot || !stopped(since_stop_ms); + } + + fn muted(since_mute_ms: u32) -> bool { + return since_mute_ms < MUTE_MS; + } + + // A muted agent still answers its own tag: muting a topic must not lose a direct call. + fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool { + return tagged || !muted(since_mute_ms); + } + + // ---- v3 ---- + + // A reaction is a courtesy: chats may switch reactions off, so the answer counts + // whether or not the reaction went through. The "seen" mark stays only when the + // turn ended in an answer; a failed turn takes it away so nobody waits for nothing. + fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool { + return answer_sent; + } + + fn keeps_seen_mark(answer_sent: bool) -> bool { + return answer_sent; + } + + // /brief: standing instructions for a group or one topic, set by an admin only. + pub const MAX_BRIEF_CHARS : u32 = 1000; + // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's. + // The narrower one wins; the two are never glued together. + fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 { + if has_topic_brief { return 2; } + if has_group_brief { return 1; } + return 0; + } + + // An empty /brief is accepted: it clears the brief, it is not refused. + fn brief_fits(chars: u32) -> bool { + return chars <= MAX_BRIEF_CHARS; + } + + fn may_set_brief(is_admin: bool, from_bot: bool) -> bool { + return is_admin && !from_bot; + } + + // A person deleted an agent's topic: the stored id is stale, so it is recreated. + // Without rights the recreate fails every time, so it is tried at most this often. + pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000; + fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool { + return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS; + } + + // /summary: the topic keeps a bounded log; a bare /summary reads the default count. + pub const SUMMARY_LOG_SIZE : u32 = 200; + pub const SUMMARY_DEFAULT : u32 = 50; + fn summary_count(asked: u32) -> u32 { + if asked == 0 { return SUMMARY_DEFAULT; } + if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; } + return asked; + } + + // ---- v4: hand-off in private, bot to bot ---- + + // Every private hand-off between two agents starts with this tag, then its kind and the + // person it is for. Only a hive agent of the same owner can send it, so the person named in + // it is trusted: the turn runs in that person's name, never the owner's. + pub const DM_TAG : str = "#a2a"; + pub const DM_ASK : u8 = 1; + pub const DM_ANSWER : u8 = 2; + // An answer to a person hands off to at most this many colleagues. + pub const MAX_DM_HANDOFFS : u32 = 2; + + // In private a person is always heard; a bot only when it is a colleague (an agent bot of + // the same owner) and its message carries the hand-off tag with a known kind. + fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool { + return !from_bot || (colleague && tag_ok); + } + + // An ask runs the colleague's turn; an answer is only carried to the person. So a private + // chain is two messages long and cannot loop, whatever the agents write. + fn dm_runs_turn(kind: u8) -> bool { + return kind == DM_ASK; + } + fn dm_carries(kind: u8) -> bool { + return kind == DM_ANSWER; + } + + // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never + // writes to a person nobody asked it about. Half an hour covers the longest turn. + pub const DM_ANSWER_WINDOW_MS : u32 = 1800000; + fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool { + if kind == DM_ASK { return true; } + if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; } + return false; + } + + // How many of the hand-offs an answer asked for are sent. + fn dm_handoffs_sent(asked: u32) -> u32 { + if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; } + return asked; + } + + test "a person calls an agent by tag, all tag, reply or its own topic" { + assert(addressed(true, false, false, false, false)); + assert(addressed(false, true, false, false, false)); + assert(addressed(false, false, true, false, false)); + assert(addressed(false, false, false, true, false)); + assert(!addressed(false, false, false, false, false)); + } + + test "a bot calls an agent only by tag or reply" { + assert(addressed(true, false, false, false, true)); + assert(addressed(false, false, true, false, true)); + assert(!addressed(false, true, false, false, true)); + assert(!addressed(false, false, false, true, true)); + assert(!addressed(false, true, false, true, true)); + } + + test "the bot chain closes after three bot messages" { + assert(chain_open(0)); + assert(chain_open(2)); + assert(!chain_open(3)); + assert(!chain_open(4)); + } + + test "a bot waits out the cooldown before answering a bot" { + assert(!cooled(0)); + assert(!cooled(4999)); + assert(cooled(5000)); + } + + test "a person is always answered and a bot only inside the limits" { + assert(may_answer(false, 9, 0)); + assert(may_answer(true, 2, 5000)); + assert(!may_answer(true, 3, 5000)); + assert(!may_answer(true, 2, 4999)); + } + + test "a creation claim lives fifteen minutes" { + assert(claim_fresh(0)); + assert(claim_fresh(899999)); + assert(!claim_fresh(900000)); + } + + test "typing is sent again four seconds after the last one" { + assert(!typing_due(0)); + assert(!typing_due(3999)); + assert(typing_due(4000)); + } + + test "on the all tag only the best fit answers at once" { + assert(speaks_on_all(0)); + assert(!speaks_on_all(1)); + assert(!speaks_on_all(7)); + } + + test "a person follows up in the same topic for two minutes without a tag" { + assert(follows_up(false, true, true, 0)); + assert(follows_up(false, true, true, 119999)); + assert(!follows_up(false, true, true, 120000)); + assert(!follows_up(false, false, true, 0)); + assert(!follows_up(false, true, false, 0)); + assert(!follows_up(true, true, true, 0)); + } + + test "a stop silences bots for ten minutes and never a person" { + assert(!may_answer_after_stop(true, 0)); + assert(!may_answer_after_stop(true, 599999)); + assert(may_answer_after_stop(true, 600000)); + assert(may_answer_after_stop(false, 0)); + } + + test "a muted agent still answers its own tag for the hour" { + assert(heard_while_muted(true, 0)); + assert(!heard_while_muted(false, 0)); + assert(!heard_while_muted(false, 3599999)); + assert(heard_while_muted(false, 3600000)); + } + + test "the answer counts even when the chat refuses reactions" { + assert(turn_ok(true, false)); + assert(turn_ok(true, true)); + assert(!turn_ok(false, true)); + assert(keeps_seen_mark(true)); + assert(!keeps_seen_mark(false)); + } + + test "the topic brief wins over the group brief, and only an admin sets one" { + assert(brief_source(true, true) == 2); + assert(brief_source(true, false) == 2); + assert(brief_source(false, true) == 1); + assert(brief_source(false, false) == 0); + assert(brief_fits(0)); + assert(brief_fits(1000)); + assert(!brief_fits(1001)); + assert(may_set_brief(true, false)); + assert(!may_set_brief(false, false)); + assert(!may_set_brief(true, true)); + } + + test "a deleted topic is recreated, at most once in ten minutes" { + assert(recreate_topic(false, false, 600000)); + assert(recreate_topic(false, true, 600000)); + assert(recreate_topic(true, false, 600000)); + assert(!recreate_topic(true, true, 600000)); + assert(!recreate_topic(true, false, 599999)); + } + + test "a summary reads fifty by default and never past the log" { + assert(summary_count(0) == 50); + assert(summary_count(1) == 1); + assert(summary_count(10) == 10); + assert(summary_count(200) == 200); + assert(summary_count(201) == 200); + } + + test "in private a colleague is heard only with the hand-off tag" { + assert(dm_heard(false, false, false)); + assert(dm_heard(true, true, true)); + assert(!dm_heard(true, false, true)); + assert(!dm_heard(true, true, false)); + } + + test "an ask runs a turn, an answer is only carried" { + assert(DM_ASK == 1); + assert(DM_ANSWER == 2); + assert(dm_runs_turn(DM_ASK)); + assert(!dm_runs_turn(DM_ANSWER)); + assert(!dm_runs_turn(0)); + assert(dm_carries(DM_ANSWER)); + assert(!dm_carries(DM_ASK)); + assert(!dm_carries(0)); + } + + test "an answer is carried only while its ask is fresh" { + assert(DM_ANSWER_WINDOW_MS == 1800000); + assert(dm_tag_ok(DM_ASK, 4000000000)); + assert(dm_tag_ok(DM_ANSWER, 0)); + assert(dm_tag_ok(DM_ANSWER, 1799999)); + assert(!dm_tag_ok(DM_ANSWER, 1800000)); + assert(!dm_tag_ok(DM_ANSWER, 4000000000)); + assert(!dm_tag_ok(0, 0)); + assert(!dm_tag_ok(3, 0)); + } + + test "an answer hands off to at most two colleagues" { + assert(MAX_DM_HANDOFFS == 2); + assert(dm_handoffs_sent(0) == 0); + assert(dm_handoffs_sent(1) == 1); + assert(dm_handoffs_sent(2) == 2); + assert(dm_handoffs_sent(3) == 2); + assert(dm_handoffs_sent(100) == 2); + } + + test "VERSION says what the header's last note says" { + assert(VERSION == 4); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-cooldown/tool.js b/apps/website/public/widgets/hive-cooldown/tool.js new file mode 100644 index 0000000000..00c21f3df3 --- /dev/null +++ b/apps/website/public/widgets/hive-cooldown/tool.js @@ -0,0 +1,32 @@ +// hive-cooldown/tool.js -- lesson 24 of queen-and-hive: the cooldown clock, /stop and /mute. The dial +// verdict is cooled(ms), the stop cards may_answer_after_stop and the mute card heard_while_muted, fn +// bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm (./logic.js); the host +// turns the minutes the reader sets into milliseconds. Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-cooldown.t27. +import { cooled, heardWhileMuted, mayAnswerAfterStop } from './logic.js' +import { K, W, ask, controls, dial, el, head, root, say, source, two, verdict } from '../hive-kit.js' + +const MIN = K('K_MS_PER_MINUTE', 60000) +const ctl = controls([ + { key: 'ms', label: say('SAY_MS'), min: 0, max: K('K_MS_MAX', 10000), step: K('K_MS_STEP', 100) }, + { key: 'stop', label: say('SAY_STOP'), min: 0, max: K('K_STOP_MAX', 20), step: 1 }, + { key: 'mute', label: say('SAY_MUTE'), min: 0, max: K('K_MUTE_MAX', 90), step: 1 }, + { key: 'tagged', label: say('SAY_TAGGED'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { ms: K('K_PRESET_MS', []), stop: K('K_PRESET_STOP', []), mute: K('K_PRESET_MUTE', []), tagged: K('K_PRESET_TAGGED', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const ok = cooled(s.ms) + const card = (word, heard) => verdict(heard ? say('SAY_HEARD') : say('SAY_SILENT'), heard ? 0 : 8, word) + out.replaceChildren( + dial({ value: s.ms, max: K('K_MS_MAX', 10000), tone: ok ? 0 : 1, big: `${(s.ms / 1000).toFixed(1)} s`, small: say('SAY_DIAL'), label: say('SAY_DIAL'), marks: [{ at: K('K_BOT_REPLY_COOLDOWN_MS', 5000), tone: 1 }] }), + verdict(ok ? say('SAY_COOLED') : say('SAY_WAIT'), ok ? 0 : 1), + head(say('SAY_STOP')), + two(card(say('SAY_STOP_CARDS', 0), mayAnswerAfterStop(true, s.stop * MIN)), card(say('SAY_STOP_CARDS', 1), mayAnswerAfterStop(false, s.stop * MIN))), + head(say('SAY_MUTE')), + card(say('SAY_MUTE_CARD'), heardWhileMuted(!!s.tagged, s.mute * MIN))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-credit/card.png b/apps/website/public/widgets/hive-credit/card.png new file mode 100644 index 0000000000..11fd10795b Binary files /dev/null and b/apps/website/public/widgets/hive-credit/card.png differ diff --git a/apps/website/public/widgets/hive-credit/hive-credit.t27 b/apps/website/public/widgets/hive-credit/hive-credit.t27 new file mode 100644 index 0000000000..06047e7938 --- /dev/null +++ b/apps/website/public/widgets/hive-credit/hive-credit.t27 @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-credit.t27 -- Lending a key earns no credit: XP follows recorded work +; Source of truth for public/widgets/hive-credit/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-credit/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/queen-contributor-keys.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 19 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the keys a contributor owns, the probes in flight and the seconds +; since the last probe. The credit meter is wallet_credit(keys added); the tiles are +; may_add_key(keys), may_start_probe(probes) and cooldown_passed(a probe ran, seconds). + +; WHAT IT DOES NOT CLAIM: it moves no money and stores no key: the wallet meter is the spec fn, and +; it answers 0. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_credit; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-credit"; +pub const TITLE : str = "Lending a key earns no credit: XP follows recorded work"; +pub const DESCRIPTION : str = "A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s."; +pub const IMAGE_ALT : str = "Credit card: a contributor with 99 keys. The keys meter is nearly full against the 100 limit and one more may be added; the wallet credit meter is empty at 0 tokens. A probe started 45 s ago waits for its 60 s cooldown; 3 of 4 probe slots are busy."; +pub const HOOK : str = "Adding a key never credits a wallet: CREDIT_WALLET is false in the spec, so XP follows only the work the Queen records, not the keys someone lends."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/queen-contributor-keys.t27 (t27 master c42f8fb40): every value on the page is wallet_credit, may_add_key, may_start_probe and cooldown_passed, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 19 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MAX_KEYS_PER_OWNER : u16 = 100; +pub const K_MAX_PROBES_IN_FLIGHT : u16 = 4; +pub const K_PROBE_COOLDOWN_SECONDS : u16 = 60; +pub const K_PROBES_MAX : u8 = 6; +pub const K_SECONDS_MAX : u8 = 120; +pub const K_PRESET_KEYS : [4]u8 = [99, 100, 3, 40]; +pub const K_PRESET_PROBES : [4]u8 = [3, 4, 0, 1]; +pub const K_PRESET_SECONDS : [4]u8 = [45, 61, 0, 90]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["99 keys, a probe 45 s ago", "At the limit", "A new contributor", "40 keys, probe cooled"]; +pub const SAY_ASK : str = "Does lending the hive a model key earn a contributor credit?"; +pub const SAY_KEYS : str = "keys this contributor owns"; +pub const SAY_PROBES : str = "probes in flight"; +pub const SAY_SECONDS : str = "seconds since the last probe (0: none yet)"; +pub const SAY_KEY_METER : str = "keys, limit {0}"; +pub const SAY_CREDIT : str = "wallet credit for those keys, tokens"; +pub const SAY_SLOT_METER : str = "probe slots in use"; +pub const SAY_ADD : str = "may add a key"; +pub const SAY_PROBE : str = "may start a probe"; +pub const SAY_COOL : str = "probe cooldown passed"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +; The verdict is filled with wallet_credit(keys): SAY_ZERO when it answers 0, SAY_CREDITED otherwise. +pub const SAY_ZERO : str = "{0} tokens: XP follows work"; +pub const SAY_CREDITED : str = "{0} tokens: the keys credit the wallet"; +pub const SAY_NOTE : str = "A key is probed before it is enabled, at most {0} at once and one per {1} s, so a burst of new keys cannot hammer a provider. Credit comes from the work the Queen records, never from the keys."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/queen-contributor-keys.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_limits_are_the_specs { + assert K_MAX_KEYS_PER_OWNER == 100; + assert K_MAX_PROBES_IN_FLIGHT == 4; + assert K_PROBE_COOLDOWN_SECONDS == 60; +} + +test the_presets_fit_the_sliders { + assert K_PRESET_KEYS[1] == K_MAX_KEYS_PER_OWNER; + assert K_PRESET_PROBES[1] <= K_PROBES_MAX; + assert K_PRESET_SECONDS[3] <= K_SECONDS_MAX; +} diff --git a/apps/website/public/widgets/hive-credit/index.html b/apps/website/public/widgets/hive-credit/index.html new file mode 100644 index 0000000000..981564befe --- /dev/null +++ b/apps/website/public/widgets/hive-credit/index.html @@ -0,0 +1,47 @@ + + + + + + +Lending a key earns no credit: XP follows recorded work · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Lending a key earns no credit: XP follows recorded work

+

A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/queen-contributor-keys.t27 (t27 master c42f8fb40): every value on the page is wallet_credit, may_add_key, may_start_probe and cooldown_passed, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 19 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-credit/logic.js b/apps/website/public/widgets/hive-credit/logic.js new file mode 100644 index 0000000000..50edd2a8a7 --- /dev/null +++ b/apps/website/public/widgets/hive-credit/logic.js @@ -0,0 +1,68 @@ +// Generated by scripts/t27-logic.mjs from queen-contributor-keys.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 5cc353dd0ad0b2bd5ee6a92498a3518a02b899e64fb73dfb2f34f67e4322b9a7 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-credit/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHARgAX8Bf2ACf38Bf2AFf39/f38Bf2ADf39/AX8DDAsAAAEBAgEBAAABAwQFAXABAQEFAwEAEAYJAX8BQYCAwAALB5ACDQZtZW1vcnkCABF0Mjdfd2FsbGV0X2NyZWRpdAAAF3QyN19tYXlfdHJ5X21vZGVsX2NoZWNrAAEUdDI3X21heV9zd2l0Y2hfbW9kZWwAAg50MjdfbW9kZWxfZml0cwADF3QyN19lbmFibGVkX2FmdGVyX3Byb2JlAAQQdDI3X3NhbWVfYmluZGluZwACE3QyN19jcmVkZW50aWFsX2ZpdHMABRN0MjdfY29vbGRvd25fcGFzc2VkAAYTdDI3X21heV9zdGFydF9wcm9iZQAHD3QyN19tYXlfYWRkX2tleQAIGHQyN19zb3VyY2VfbWF0Y2hlc19pbmRleAAJEXQyN190cnVzdGVkX3Byb3h5AAoKsAELBABBAAsHACAAQQJJCwcAIAAgAXELDgAgAEF/akGAAUkgAXELIQEBf0EAIQUCQCABQQFxDQAgAiADcSAEcSAAciEFCyAFCw4AIABBf2pBgCBJIAFxCw0AIABBAXMgAUE8S3ILBwAgAEEESQsIACAAQeQASQssAQF/QQEhAgJAAkAgAEEBcUUNACABQQBIDQELIABBf3MgAUF/SnEhAgsgAgsNACAAQR9LIAFxIAJxCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function trustedProxy(configuredBytes, tokenMatches, subjectValid) { + return (W.t27_trusted_proxy(int(configuredBytes, 0, 4294967295, "trusted_proxy.configured_bytes"), tokenMatches ? 1 : 0, subjectValid ? 1 : 0) & 1) !== 0; +} +function sourceMatchesIndex(managed, index) { + return (W.t27_source_matches_index(managed ? 1 : 0, int(index, -2147483648, 2147483647, "source_matches_index.index")) & 1) !== 0; +} +function mayAddKey(owned) { + return (W.t27_may_add_key(int(owned, 0, 4294967295, "may_add_key.owned")) & 1) !== 0; +} +function mayStartProbe(active) { + return (W.t27_may_start_probe(int(active, 0, 4294967295, "may_start_probe.active")) & 1) !== 0; +} +function cooldownPassed(hasProbe, elapsedSeconds) { + return (W.t27_cooldown_passed(hasProbe ? 1 : 0, int(elapsedSeconds, 0, 4294967295, "cooldown_passed.elapsed_seconds")) & 1) !== 0; +} +function credentialFits(bytes, printableWithoutSpace) { + return (W.t27_credential_fits(int(bytes, 0, 4294967295, "credential_fits.bytes"), printableWithoutSpace ? 1 : 0) & 1) !== 0; +} +function sameBinding(subjectMatches, fingerprintMatches) { + return (W.t27_same_binding(subjectMatches ? 1 : 0, fingerprintMatches ? 1 : 0) & 1) !== 0; +} +function enabledAfterProbe(wasEnabled, invalid, success, requestEnable, sameRevision) { + return (W.t27_enabled_after_probe(wasEnabled ? 1 : 0, invalid ? 1 : 0, success ? 1 : 0, requestEnable ? 1 : 0, sameRevision ? 1 : 0) & 1) !== 0; +} +function modelFits(chars, allowedChars) { + return (W.t27_model_fits(int(chars, 0, 4294967295, "model_fits.chars"), allowedChars ? 1 : 0) & 1) !== 0; +} +function maySwitchModel(ownsProviderKey, calledTool) { + return (W.t27_may_switch_model(ownsProviderKey ? 1 : 0, calledTool ? 1 : 0) & 1) !== 0; +} +function mayTryModelCheck(attempt) { + return (W.t27_may_try_model_check(int(attempt, 0, 4294967295, "may_try_model_check.attempt")) & 1) !== 0; +} +function walletCredit(added) { + return W.t27_wallet_credit(int(added, 0, 4294967295, "wallet_credit.added")) >>> 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + cooldownPassed, + credentialFits, + enabledAfterProbe, + mayAddKey, + mayStartProbe, + maySwitchModel, + mayTryModelCheck, + modelFits, + sameBinding, + sourceMatchesIndex, + trustedProxy, + walletCredit +}; diff --git a/apps/website/public/widgets/hive-credit/logic.t27 b/apps/website/public/widgets/hive-credit/logic.t27 new file mode 100644 index 0000000000..dd47361961 --- /dev/null +++ b/apps/website/public/widgets/hive-credit/logic.t27 @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/queen-contributor-keys.t27 -- private contributor key lifecycle. +// Host: BrowserOS trios/agent-server/apps/server/src/api/services/queen-contributor-keys.ts. +// Trace: gHashTag/t27#5472; gHashTag/999-multibots-telegraf#3251. +// Generated: t27c gen-ts supplies the host policy constants. +// Claim status: executable contract; PostgreSQL and live provider evidence are separate. +// XP follows work recorded by the Queen. Adding credentials never credits a wallet. +// Negative managed indices and nonnegative environment indices share one history. +// v1 (2026-10-01): trusted proxy, persistent ownership and bounded provider probes. +// v2 (2026-10-02): one model for all of an owner's keys of a provider, accepted only +// after a key of that provider called a tool with it. Probe timeout 15 s -> 90 s: +// z-ai/glm-5.3 on NVIDIA answered in 20-46 s and glm-4.5-flash on Z.ai in up to 87 s. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::queen_contributor_keys { + pub const KIND : str = "automation"; + pub const ID : str = "queen-contributor-keys"; + pub const REPO : str = "BrowserOS"; + pub const VERSION : u8 = 2; + + pub const PROBE_TIMEOUT_MS : u32 = 90000; + pub const PROBE_MAX_TOKENS : u16 = 16; + pub const MAX_PROBES_IN_FLIGHT : u16 = 4; + pub const PROBE_COOLDOWN_SECONDS : u16 = 60; + pub const MAX_KEYS_PER_OWNER : u16 = 100; + pub const MAX_KEY_BYTES : u16 = 4096; + pub const MAX_BODY_BYTES : u16 = 8192; + pub const LABEL_LIMIT : u16 = 80; + pub const PROXY_TOKEN_MIN_BYTES : u16 = 32; + pub const OWNER_REASSIGNMENT : bool = false; + pub const CREDIT_WALLET : bool = false; + pub const ENVIRONMENT_INDICES_COMPACTED : bool = false; + pub const NVIDIA_URL : str = "https://integrate.api.nvidia.com/v1"; + pub const NVIDIA_MODEL : str = "nvidia/nemotron-3-ultra-550b-a55b"; + pub const ZAI_URL : str = "https://api.z.ai/api/paas/v4"; + pub const ZAI_MODEL : str = "glm-4.5-flash"; + pub const MODEL_LIMIT : u16 = 128; + pub const MODEL_PROBE_MAX_TOKENS : u16 = 512; + pub const MODEL_CHECK_ATTEMPTS : u16 = 2; + pub const MODEL_LIST_LIMIT : u16 = 500; + pub const MODEL_LIST_CACHE_SECONDS : u16 = 600; + + fn trusted_proxy(configured_bytes: u32, token_matches: bool, subject_valid: bool) -> bool { + return configured_bytes >= PROXY_TOKEN_MIN_BYTES && token_matches && subject_valid; + } + + fn source_matches_index(managed: bool, index: i32) -> bool { + return (managed && index < 0) || (!managed && index >= 0); + } + + fn may_add_key(owned: u32) -> bool { + return owned < MAX_KEYS_PER_OWNER; + } + + fn may_start_probe(active: u32) -> bool { + return active < MAX_PROBES_IN_FLIGHT; + } + + fn cooldown_passed(has_probe: bool, elapsed_seconds: u32) -> bool { + return !has_probe || elapsed_seconds > PROBE_COOLDOWN_SECONDS; + } + + fn credential_fits(bytes: u32, printable_without_space: bool) -> bool { + return bytes > 0 && bytes <= MAX_KEY_BYTES && printable_without_space; + } + + fn same_binding(subject_matches: bool, fingerprint_matches: bool) -> bool { + return subject_matches && fingerprint_matches; + } + + fn enabled_after_probe(was_enabled: bool, invalid: bool, success: bool, request_enable: bool, same_revision: bool) -> bool { + if invalid { return false; } + if success && request_enable && same_revision { return true; } + return was_enabled; + } + + fn model_fits(chars: u32, allowed_chars: bool) -> bool { + return chars > 0 && chars <= MODEL_LIMIT && allowed_chars; + } + + fn may_switch_model(owns_provider_key: bool, called_tool: bool) -> bool { + return owns_provider_key && called_tool; + } + + fn may_try_model_check(attempt: u32) -> bool { + return attempt < MODEL_CHECK_ATTEMPTS; + } + + fn wallet_credit(added: u32) -> u32 { + if CREDIT_WALLET { return added; } + return 0; + } + + test "only a configured matching proxy and valid person subject are trusted" { + assert(trusted_proxy(32, true, true)); + assert(!trusted_proxy(31, true, true)); + assert(!trusted_proxy(32, false, true)); + assert(!trusted_proxy(32, true, false)); + } + + test "managed indices are negative and environment history keeps its nonnegative indices" { + assert(source_matches_index(true, -1)); + assert(!source_matches_index(true, 0)); + assert(!source_matches_index(false, -1)); + assert(source_matches_index(false, 0)); + assert(source_matches_index(false, 10000)); + assert(!ENVIRONMENT_INDICES_COMPACTED); + } + + test "the owner key limit is checked before another key is stored" { + assert(may_add_key(0)); + assert(may_add_key(99)); + assert(!may_add_key(100)); + } + + test "probe fanout and per-key cooldown are bounded" { + assert(may_start_probe(3)); + assert(!may_start_probe(4)); + assert(cooldown_passed(false, 0)); + assert(!cooldown_passed(true, 59)); + assert(!cooldown_passed(true, 60)); + assert(cooldown_passed(true, 61)); + } + + test "credentials are bounded nonempty printable values" { + assert(credential_fits(1, true)); + assert(credential_fits(4096, true)); + assert(!credential_fits(0, true)); + assert(!credential_fits(4097, true)); + assert(!credential_fits(32, false)); + } + + test "an existing binding cannot change its owner or credential" { + assert(same_binding(true, true)); + assert(!same_binding(false, true)); + assert(!same_binding(true, false)); + assert(!OWNER_REASSIGNMENT); + } + + test "a failed probe disables an invalid key and a concurrent disable wins" { + assert(!enabled_after_probe(true, true, false, false, true)); + assert(enabled_after_probe(false, false, true, true, true)); + assert(!enabled_after_probe(false, false, true, true, false)); + assert(!enabled_after_probe(false, false, true, false, true)); + assert(enabled_after_probe(true, false, false, false, true)); + } + + test "adding credentials never credits a wallet" { + assert(wallet_credit(1) == 0); + assert(wallet_credit(100) == 0); + } + + test "a model name is a bounded nonempty identifier" { + assert(model_fits(1, true)); + assert(model_fits(128, true)); + assert(!model_fits(0, true)); + assert(!model_fits(129, true)); + assert(!model_fits(12, false)); + } + + test "a model switch needs an owned key of the provider and a tool call" { + assert(may_switch_model(true, true)); + assert(!may_switch_model(false, true)); + assert(!may_switch_model(true, false)); + } + + test "a busy or refused key passes the model check to the next one, a bounded number of times" { + assert(may_try_model_check(0)); + assert(may_try_model_check(1)); + assert(!may_try_model_check(2)); + } + + test "a probe waits long enough for a slow reasoning model" { + assert(PROBE_TIMEOUT_MS >= 60000); + assert(MODEL_PROBE_MAX_TOKENS > PROBE_MAX_TOKENS); + } + + test "VERSION says what the header's last note says" { + assert(VERSION == 2); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-credit/tool.js b/apps/website/public/widgets/hive-credit/tool.js new file mode 100644 index 0000000000..865cd6ef23 --- /dev/null +++ b/apps/website/public/widgets/hive-credit/tool.js @@ -0,0 +1,37 @@ +// hive-credit/tool.js -- lesson 19 of queen-and-hive: lending a key earns no credit. The credit meter +// and the verdict under it are wallet_credit (its answer picks and fills the verdict's words), and the +// tiles may_add_key, may_start_probe and cooldown_passed, fn bodies of gHashTag/t27 +// specs/automation/queen-contributor-keys.t27 compiled to wasm (./logic.js). Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-credit.t27. +import { cooldownPassed, mayAddKey, mayStartProbe, walletCredit } from './logic.js' +import { K, W, ask, controls, el, fill, meters, root, say, source, tiles, verdict } from '../hive-kit.js' + +const KMAX = K('K_MAX_KEYS_PER_OWNER', 100), PMAX = K('K_MAX_PROBES_IN_FLIGHT', 4) +const ctl = controls([ + { key: 'keys', label: say('SAY_KEYS'), min: 0, max: KMAX, step: 1 }, + { key: 'probes', label: say('SAY_PROBES'), min: 0, max: K('K_PROBES_MAX', 6), step: 1 }, + { key: 'seconds', label: say('SAY_SECONDS'), min: 0, max: K('K_SECONDS_MAX', 120), step: 1 }, +], { names: W.SAY_PRESET_NAMES, values: { keys: K('K_PRESET_KEYS', []), probes: K('K_PRESET_PROBES', []), seconds: K('K_PRESET_SECONDS', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, fill(say('SAY_NOTE'), PMAX, K('K_PROBE_COOLDOWN_SECONDS', 60))), source()) +const yes = (v) => (v ? say('SAY_YES') : say('SAY_NO')) + +function draw(s) { + const credit = walletCredit(s.keys) + const add = mayAddKey(s.keys), probe = mayStartProbe(s.probes), cool = cooldownPassed(s.seconds > 0, s.seconds) + out.replaceChildren( + meters([ + { label: fill(say('SAY_KEY_METER'), KMAX), value: s.keys, max: KMAX, text: `${s.keys} / ${KMAX}`, tone: add ? 2 : 6 }, + { label: say('SAY_CREDIT'), value: credit, max: KMAX, text: String(credit), tone: 1 }, + { label: say('SAY_SLOT_METER'), value: s.probes, max: Math.max(PMAX, s.probes), text: `${s.probes} / ${PMAX}`, tone: probe ? 0 : 6 }, + ]), + verdict(fill(say(credit ? 'SAY_CREDITED' : 'SAY_ZERO'), credit), credit ? 0 : 1), + tiles([ + { label: say('SAY_ADD'), value: yes(add), tone: add ? 0 : 6 }, + { label: say('SAY_PROBE'), value: yes(probe), tone: probe ? 0 : 6 }, + { label: say('SAY_COOL'), value: yes(cool), tone: cool ? 0 : 1 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-drain/card.png b/apps/website/public/widgets/hive-drain/card.png new file mode 100644 index 0000000000..6702bb8fb0 Binary files /dev/null and b/apps/website/public/widgets/hive-drain/card.png differ diff --git a/apps/website/public/widgets/hive-drain/hive-drain.t27 b/apps/website/public/widgets/hive-drain/hive-drain.t27 new file mode 100644 index 0000000000..74859a223f --- /dev/null +++ b/apps/website/public/widgets/hive-drain/hive-drain.t27 @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-drain.t27 -- A deploy mid-flight: what happens to a running bee +; Source of truth for public/widgets/hive-drain/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-drain/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/drain.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 9 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the seconds waited, marks the work ended or not, and sets the +; platform drain. The cap is drain_cap_seconds(runner 1800, platform); each of the five kinds is +; drain_verdict(kind, ended, waited, cap). + +; WHAT IT DOES NOT CLAIM: the runner drain of 1800 s is the value the spec names for today; the +; page sends no signal. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_drain; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-drain"; +pub const TITLE : str = "A deploy mid-flight: what happens to a running bee"; +pub const DESCRIPTION : str = "The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store."; +pub const IMAGE_ALT : str = "Drain card at 1800 seconds of waiting, the cap. Five rows: bee HAND BACK in violet, review STOP in orange, round, job and wait LEAVE in sky blue. A violet meter shows 1800 of the 1800 s cap, the runner drain 1800 against the platform 1830."; +pub const HOOK : str = "A deploy does not cut a bee short: it is held up to the cap, then what it wrote is salvaged into its branch and its issue is free at once."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/drain.t27 (t27 master c42f8fb40): every value on the page is drain_verdict and drain_cap_seconds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 9 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_WAIT_MAX : u16 = 2400; +pub const K_WAIT_STEP : u8 = 60; +pub const K_RUNNER_DRAIN : u16 = 1800; +pub const K_PLATFORM_MAX : u16 = 3600; +pub const K_PLATFORM_STEP : u8 = 30; +pub const K_PRESET_WAIT : [4]u16 = [1800, 600, 60, 1800]; +pub const K_PRESET_ENDED : [4]u8 = [0, 0, 1, 0]; +pub const K_PRESET_PLATFORM : [4]u16 = [1830, 1830, 1830, 1000]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["At the cap", "Ten minutes in", "Work already ended", "A short platform drain"]; +pub const SAY_ASK : str = "What happens to a bee halfway through its task when the Queen is redeployed?"; +pub const SAY_WAIT : str = "seconds the drain has waited"; +pub const SAY_ENDED : str = "the work already ended"; +pub const SAY_PLATFORM : str = "platform drain, seconds"; +pub const SAY_KIND_NAMES : [5]str = ["a bee", "a review", "a round", "a job step", "a wait row"]; +pub const SAY_VERDICT_NAMES : [5]str = ["DONE", "LEAVE", "STOP", "HOLD", "HAND BACK"]; +pub const SAY_VERDICT_LINES : [5]str = ["nothing is owed", "the store keeps it; the next process takes it up", "not waited for; stopped when the process exits", "the drain waits for it to end", "stopped, salvaged into its branch, its issue free now"]; +pub const SAY_CAP : str = "cap, seconds"; +pub const SAY_WAITED : str = "waited"; +pub const SAY_NOTE : str = "A review is not held: a stop loses at most one review, which the next process redoes. A bee is held because what it has done lives in its conversation, which no row keeps."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/drain.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_reaches_past_the_cap { + assert K_WAIT_MAX > K_RUNNER_DRAIN; + assert K_PRESET_WAIT[0] == K_RUNNER_DRAIN; +} + +test the_platform_drain_is_in_range { + assert K_PRESET_PLATFORM[0] <= K_PLATFORM_MAX; + assert K_PRESET_PLATFORM[3] < K_RUNNER_DRAIN; +} diff --git a/apps/website/public/widgets/hive-drain/index.html b/apps/website/public/widgets/hive-drain/index.html new file mode 100644 index 0000000000..b0a7ab42fc --- /dev/null +++ b/apps/website/public/widgets/hive-drain/index.html @@ -0,0 +1,47 @@ + + + + + + +A deploy mid-flight: what happens to a running bee · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

A deploy mid-flight: what happens to a running bee

+

The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/drain.t27 (t27 master c42f8fb40): every value on the page is drain_verdict and drain_cap_seconds, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 9 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-drain/logic.js b/apps/website/public/widgets/hive-drain/logic.js new file mode 100644 index 0000000000..e9cef0c895 --- /dev/null +++ b/apps/website/public/widgets/hive-drain/logic.js @@ -0,0 +1,76 @@ +// Generated by scripts/t27-logic.mjs from drain.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: ae64ef302f98301a0594298d43848603ad55177e387f72d7edcf0b863f5a081a +// Imported by scripts/widget-logic.mjs for public/widgets/hive-drain/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABGwRgAn9/AX9gAX8Bf2ADf39/AX9gBH9/f38BfwMODQAAAQAAAAIAAAMBAQEEBQFwAQEBBQMBABAGCQF/AUGAgMAACweuAg8GbWVtb3J5AgAYdDI3X3BpY2t1cF9hZnRlcl9zZWNvbmRzAAAPdDI3X3JlY2xhaW1hYmxlAAERdDI3X2xlYXNlX2V4cGlyZWQAAg90MjdfdHVybl9zaWduYWwAAw90Mjdfc3RvcF9zaWduYWwABBt0Mjdfd2FpdF9mcmVlX2FmdGVyX3NlY29uZHMAABp0MjdfYmVlX2ZyZWVfYWZ0ZXJfc2Vjb25kcwAFDXQyN19leGl0X3N0ZXAABhV0MjdfZHJhaW5fY2FwX3NlY29uZHMABxJ0MjdfdGFrZXNfbmV3X3dvcmsACBF0MjdfZHJhaW5fdmVyZGljdAAJEXQyN19yZWRvX2lzX2NoZWFwAAoMdDI3X2JvdW5kX29mAAsNdDI3X3Jvd19rZWVwcwAMCs4CDRcAQTwgAWtBACABQTxJG0EAIABBAXEbCw4AIABBf3MgAUGzAUtxCwgAIABBswFLCxMAQeMAQQIgACABQZAcIAEbSRsLLwACQCABDQBBAg8LAkAgAA0AQQEPCwJAIAFBf0cNAEHjAA8LQeMAQQIgACABSRsLGQBBAEEAQbQBIAFrIAFBswFLGyAAQQFxGwsUACAAQQBHIAFBAEcgAkGIJ0lxcgsvAQF/AkAgAQ0AIAAPC0EAIQICQCABQRpJDQAgACABQWdqIgEgACABSRshAgsgAgsNACAAQX9zIAFBAEdxC0IBAX9BASEEAkAgAUEBcUUNAEEADwsCQCAAQX5qQf8BcUEDSQ0AAkAgAEEBRw0AQQIPC0EDQQQgAiADSRshBAsgBAsLACAAQX9qQawCSQsNAEGsAkEAIABBAUYbCw4AIABBfmpB/wFxQQNJCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function rowKeeps(kind) { + return (W.t27_row_keeps(int(kind, 0, 255, "row_keeps.kind")) & 1) !== 0; +} +function boundOf(kind) { + return W.t27_bound_of(int(kind, 0, 255, "bound_of.kind")) >>> 0; +} +function redoIsCheap(boundSeconds) { + return (W.t27_redo_is_cheap(int(boundSeconds, 0, 4294967295, "redo_is_cheap.bound_seconds")) & 1) !== 0; +} +function drainVerdict(kind, ended, waitedSeconds, capSeconds) { + return W.t27_drain_verdict(int(kind, 0, 255, "drain_verdict.kind"), ended ? 1 : 0, int(waitedSeconds, 0, 4294967295, "drain_verdict.waited_seconds"), int(capSeconds, 0, 4294967295, "drain_verdict.cap_seconds")) & 255; +} +function takesNewWork(startsABeeHere, holding) { + return (W.t27_takes_new_work(startsABeeHere ? 1 : 0, int(holding, 0, 4294967295, "takes_new_work.holding")) & 1) !== 0; +} +function drainCapSeconds(runnerDrainSeconds, platformSeconds) { + return W.t27_drain_cap_seconds(int(runnerDrainSeconds, 0, 4294967295, "drain_cap_seconds.runner_drain_seconds"), int(platformSeconds, 0, 4294967295, "drain_cap_seconds.platform_seconds")) >>> 0; +} +function exitStep(holding, stopping, sinceStopMs) { + return W.t27_exit_step(int(holding, 0, 4294967295, "exit_step.holding"), int(stopping, 0, 4294967295, "exit_step.stopping"), int(sinceStopMs, 0, 4294967295, "exit_step.since_stop_ms")) & 255; +} +function beeFreeAfterSeconds(handedBack, sinceRenewal) { + return W.t27_bee_free_after_seconds(handedBack ? 1 : 0, int(sinceRenewal, 0, 4294967295, "bee_free_after_seconds.since_renewal")) >>> 0; +} +function waitFreeAfterSeconds(claimed, sinceClaim) { + return W.t27_wait_free_after_seconds(claimed ? 1 : 0, int(sinceClaim, 0, 4294967295, "wait_free_after_seconds.since_claim")) >>> 0; +} +function stopSignal(waitedMs, timeoutMs) { + return W.t27_stop_signal(int(waitedMs, 0, 4294967295, "stop_signal.waited_ms"), int(timeoutMs, 0, 4294967295, "stop_signal.timeout_ms")) & 255; +} +function turnSignal(turnAgeSeconds, maxSeconds) { + return W.t27_turn_signal(int(turnAgeSeconds, 0, 4294967295, "turn_signal.turn_age_seconds"), int(maxSeconds, 0, 4294967295, "turn_signal.max_seconds")) & 255; +} +function leaseExpired(secondsSinceRenewal) { + return (W.t27_lease_expired(int(secondsSinceRenewal, 0, 4294967295, "lease_expired.seconds_since_renewal")) & 1) !== 0; +} +function reclaimable(finished, secondsSinceRenewal) { + return (W.t27_reclaimable(finished ? 1 : 0, int(secondsSinceRenewal, 0, 4294967295, "reclaimable.seconds_since_renewal")) & 1) !== 0; +} +function pickupAfterSeconds(claimed, secondsSinceClaim) { + return W.t27_pickup_after_seconds(claimed ? 1 : 0, int(secondsSinceClaim, 0, 4294967295, "pickup_after_seconds.seconds_since_claim")) >>> 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + beeFreeAfterSeconds, + boundOf, + drainCapSeconds, + drainVerdict, + exitStep, + leaseExpired, + pickupAfterSeconds, + reclaimable, + redoIsCheap, + rowKeeps, + stopSignal, + takesNewWork, + turnSignal, + waitFreeAfterSeconds +}; diff --git a/apps/website/public/widgets/hive-drain/logic.t27 b/apps/website/public/widgets/hive-drain/logic.t27 new file mode 100644 index 0000000000..8a5ceee5bd --- /dev/null +++ b/apps/website/public/widgets/hive-drain/logic.t27 @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/drain.t27 -- when a draining Queen process may exit: what it waits for, what it stops, what it leaves +; Owner's night loop, 2026-10-10 (gHashTag/trios#1729 item 2): "every deploy still freezes the swarm for +; 1800 s; with durable jobs and waits and stoppable turns, a spec can bound the drain by in-flight turns". +; Measured before this card (gHashTag/trios trios/agent-server, branch queen): +; - the drain is not a fixed wait. On SIGTERM the server drains its own bee runner (queen-runner.ts +; drainBeeRunner): it takes no new order and waits while any bee it carries still runs, up to +; TRIOS_BEE_RUNNER_DRAIN_SECONDS = 1800, then salvages the branch of each bee still running and exits +; about 2 s later. Rounds, the reviewer and the wait scheduler run on through that wait +; (roundsThroughDrain, BrowserOS#546) and are cut by the exit. Railway's +; RAILWAY_DEPLOYMENT_DRAINING_SECONDS = 1830 is only the SIGKILL that follows; the service has a +; volume, so the next container starts when this one ends. +; - so the Queen's own bees are what the drain waits for, and they usually outlive it. GitHub's +; deployment statuses for the Queen's service, 2026-10-08 23:14Z to 2026-10-10 12:01Z: 7 of 8 deploys +; took 30.4 to 33.7 min from start to live, and one took 2.8 min (00:44Z, a quiet night). +; - the 1800 s protects a bee that ends within 30 min of SIGTERM. A bee still running then is cut +; anyway (salvaged, its conversation lost), and its row stays claimed until its task lease lapses. +; Owner rules this card keeps (2026-10-08): a deploy must not cut a bee short, and must not idle the +; swarm; never shorten the drain without asking (a 120 s drain was tried and reverted in 5 min). +; How others stop for a deploy: OTP stops children in reverse order with a shutdown time each, then +; kills; Akka Cluster Sharding hands a shard off within 60 s and warns that Kubernetes' 30 s grace can +; be shorter; Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds, then SIGKILL; DBOS, +; Temporal and Restate keep a workflow's progress in the store, so a stopped process loses only the +; step it was on. +; What is reused, not restated: +; - actors.t27 turn_signal (a turn's own bound, 0 meaning TURN_MAX_SECONDS) and stop_signal +; (X_SHUTDOWN, then X_KILL past a timeout); +; - turn_stop.t27: a stopped turn gets its abort, TURN_STOP_GRACE_MS, then its escalation, and its lane +; is free within LANE_FREE_MS; +; - waits.t27 section 7: a deploy writes nothing to a wait; the next process takes it up +; (pickup_after_seconds); +; - jobs.t27: a job holds no lane, and an effect runs through the effects journal, so a process that +; stops mid-step never repeats it; +; - control.t27 section 2: a runner's task lease lapses TASK_LEASE_TTL_SECONDS after its last renewal, +; and only then is its task reclaimable. +; Not here: the store, the signals, the salvage (the runtime, gHashTag/trios). +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenDrain; + +use queen::actors::turn_signal; +use queen::actors::stop_signal; +use queen::control::reclaimable; +use queen::waits::pickup_after_seconds; + +; --- 1. What a draining process holds --------------------------------------------------------------- + +/// The kinds of work a Queen process can hold when SIGTERM arrives. +/// WK_BEE: a bee this process runs for its runner (queen-runner.ts), a turn of up to an hour. +/// WK_REVIEW: one review (reviewer.t27), a turn bounded by REVIEW_ROW_SECONDS. +/// WK_ROUND: a Queen round, a reconcile: it re-reads the store and decides again. +/// WK_JOB: a step of a job (jobs.t27). +/// WK_WAIT: a wait row this process claimed (waits.t27). +pub const WK_BEE : u8 = 0; +pub const WK_REVIEW : u8 = 1; +pub const WK_ROUND : u8 = 2; +pub const WK_JOB : u8 = 3; +pub const WK_WAIT : u8 = 4; +pub const WORK_KINDS : u8 = 5; + +/// actors.t27 X_KILL and X_NONE, written as literals here (stop_signal and turn_signal answer them). +pub const X_KILL_LIT : u8 = 2; +pub const X_NONE_LIT : u8 = 99; + +/// Whether the store keeps this work as it stands, so another process continues it from there and +/// a stop throws nothing away. A wait is a row a deploy never writes (waits.t27 section 7). A job is +/// a row and its effects are journaled (jobs.t27). A round is a reconcile: everything it decided is +/// decided again from the store by the next round, and every effect it made is journaled. A review +/// and a bee are turns: their row stays, but what the turn has done so far is in its process. +pub fn row_keeps(kind: u8) -> bool { + if (kind == WK_WAIT) { return true; } + if (kind == WK_JOB) { return true; } + if (kind == WK_ROUND) { return true; } + return false; +} + +/// A turn's own bound in seconds, as actors.t27 turn_signal reads it (0 is TURN_MAX_SECONDS, 3600). +/// A review is reviewer.t27 REVIEW_ROW_SECONDS (300, a literal here). A bee names none. +pub const REVIEW_BOUND_SECONDS : u32 = 300; + +pub fn bound_of(kind: u8) -> u32 { + if (kind == WK_REVIEW) { return REVIEW_BOUND_SECONDS; } + return 0; +} + +/// The most work a drain may throw away to end sooner: one review. A turn whose own timer kills it +/// within this many seconds (turn_signal says X_KILL at this age) loses at most that much to a stop, +/// and the next process redoes it from its row. A bee's turn does not qualify: its bound is an hour, +/// and what it has done lives in its conversation, which no row keeps. Chosen, not measured. +pub const STOP_LOSS_SECONDS : u32 = 300; + +pub fn redo_is_cheap(bound_seconds: u32) -> bool { + return turn_signal(STOP_LOSS_SECONDS, bound_seconds) == X_KILL_LIT; +} + +; --- 2. What the drain owes each piece of work -------------------------------------------------------- + +/// DV_DONE: its work ended; nothing is owed. +/// DV_LEAVE: the store keeps it (row_keeps); the process exits without it and the next one takes it up. +/// DV_STOP: the drain does not wait for it. It runs on while something else holds the process, and is +/// stopped when the process exits, through turn_stop.t27 (its abort, the grace, the escalation). +/// DV_HOLD: the drain waits for it to end on its own. +/// DV_HAND_BACK: held to the cap and still running: it is stopped as DV_STOP is, what it wrote is +/// salvaged into its branch and stored, and its row is ended now, so its issue is free at once. +pub const DV_DONE : u8 = 0; +pub const DV_LEAVE : u8 = 1; +pub const DV_STOP : u8 = 2; +pub const DV_HOLD : u8 = 3; +pub const DV_HAND_BACK : u8 = 4; + +/// A bee is held until it ends or the cap passes: the owner's rule that a deploy does not cut a bee +/// short, kept to the same cap as before this card. A review is not held: a stop loses at most one +/// review, which the next process redoes. Rows are left. +pub fn drain_verdict(kind: u8, ended: bool, waited_seconds: u32, cap_seconds: u32) -> u8 { + if (ended) { return DV_DONE; } + if (row_keeps(kind)) { return DV_LEAVE; } + if (redo_is_cheap(bound_of(kind))) { return DV_STOP; } + if (waited_seconds >= cap_seconds) { return DV_HAND_BACK; } + return DV_HOLD; +} + +; --- 3. What keeps working while something holds ------------------------------------------------------ + +/// Whether a source of work starts anything new during the drain. Nothing that would start a bee in +/// this process: that bee would hold the drain for up to its hour. Everything else keeps going while +/// a held bee keeps the process alive, because the next process cannot start before this one ends +/// (one instance, one volume), and stopping the rounds then idled the swarm for the whole drain +/// (measured 2026-10-08, BrowserOS#546). Once nothing holds, the process is leaving: nothing starts. +pub fn takes_new_work(starts_a_bee_here: bool, holding: u32) -> bool { + if (starts_a_bee_here) { return false; } + return holding > 0; +} + +; --- 4. The hard cap ---------------------------------------------------------------------------------- + +/// What must still fit after the cap, before the platform's SIGKILL: the hand-back of every held bee +/// (its abort and grace, the salvage into its branch, the bundle, the row), run side by side under one +/// deadline, then the server's own close (main.ts gives it 2 s). Chosen, not measured. +pub const HAND_BACK_SECONDS : u32 = 20; +pub const EXIT_SECONDS : u32 = 5; + +/// The cap: the runner's drain (TRIOS_BEE_RUNNER_DRAIN_SECONDS), never so late that the hand-back and +/// the exit cannot finish before the platform kills the process (RAILWAY_DEPLOYMENT_DRAINING_SECONDS). +/// A platform drain of 0 is one this process cannot read: the runner's drain stands, as before. +/// With today's 1800 and 1830 the cap is 1800: this card cuts no bee sooner than the drain it replaces. +pub fn drain_cap_seconds(runner_drain_seconds: u32, platform_seconds: u32) -> u32 { + if (platform_seconds == 0) { return runner_drain_seconds; } + if (platform_seconds <= HAND_BACK_SECONDS + EXIT_SECONDS) { return 0; } + let latest : u32 = platform_seconds - HAND_BACK_SECONDS - EXIT_SECONDS; + if (runner_drain_seconds < latest) { return runner_drain_seconds; } + return latest; +} + +; --- 5. The exit -------------------------------------------------------------------------------------- + +/// turn_stop.t27's promises, written as literals here: the grace after the abort, and the time by +/// which a stopped turn's lane is free. +pub const TURN_STOP_GRACE_MS : u32 = 3000; +pub const LANE_FREE_MS : u32 = 5000; + +pub const EXIT_NOW : u8 = 0; +pub const EXIT_WAIT : u8 = 1; + +/// The process exits once nothing holds it and every turn it stopped has had its stop land: its abort +/// at once, the grace, the escalation (stop_signal with the grace says X_KILL by LANE_FREE_MS). A +/// process that stopped nothing exits at once. +pub fn exit_step(holding: u32, stopping: u32, since_stop_ms: u32) -> u8 { + if (holding > 0) { return EXIT_WAIT; } + if (stopping == 0) { return EXIT_NOW; } + if (since_stop_ms >= LANE_FREE_MS) { return EXIT_NOW; } + return EXIT_WAIT; +} + +; --- 6. When the work is free for another process ----------------------------------------------------- + +/// control.t27 TASK_LEASE_TTL_SECONDS, written as a literal here (pinned by the invariant below). +pub const TASK_LEASE_TTL_LIT : u32 = 180; + +/// Seconds after the exit before another process may take the issue of a bee the drain stopped. +/// Handed back, its row is ended at the cap: free at once. Left claimed, as before this card, it waits +/// until its task lease lapses (control.t27 reclaimable), counted from the last renewal, which stopped +/// with the process. +pub fn bee_free_after_seconds(handed_back: bool, since_renewal: u32) -> u32 { + if (handed_back) { return 0; } + if (reclaimable(false, since_renewal)) { return 0; } + return TASK_LEASE_TTL_LIT - since_renewal; +} + +/// Seconds after the exit before another process may take a wait row this process had claimed: the +/// rest of its claim (waits.t27 pickup_after_seconds). The drain writes nothing to it. +pub fn wait_free_after_seconds(claimed: bool, since_claim: u32) -> u32 { + return pickup_after_seconds(claimed, since_claim); +} + +; --- tests -------------------------------------------------------------------------------------------- + +test rows_are_left_to_the_next_process { + assert row_keeps(WK_WAIT) == true; + assert row_keeps(WK_JOB) == true; + assert row_keeps(WK_ROUND) == true; + assert row_keeps(WK_REVIEW) == false; + assert row_keeps(WK_BEE) == false; + assert drain_verdict(WK_WAIT, false, 0, 1800) == DV_LEAVE; + assert drain_verdict(WK_JOB, false, 0, 1800) == DV_LEAVE; + assert drain_verdict(WK_ROUND, false, 0, 1800) == DV_LEAVE; + // past the cap too: a row is never handed back, it is simply there + assert drain_verdict(WK_WAIT, false, 1800, 1800) == DV_LEAVE; +} + +test a_review_is_stopped_at_the_exit_not_waited_for { + assert bound_of(WK_REVIEW) == 300; + assert redo_is_cheap(300) == true; + assert redo_is_cheap(301) == false; + assert drain_verdict(WK_REVIEW, false, 0, 1800) == DV_STOP; + assert drain_verdict(WK_REVIEW, false, 1800, 1800) == DV_STOP; +} + +test a_bee_is_held_until_it_ends_or_the_cap { + assert bound_of(WK_BEE) == 0; + // its bound is TURN_MAX_SECONDS: a stop could throw away an hour + assert redo_is_cheap(0) == false; + assert redo_is_cheap(3600) == false; + assert drain_verdict(WK_BEE, false, 0, 1800) == DV_HOLD; + assert drain_verdict(WK_BEE, false, 1799, 1800) == DV_HOLD; + assert drain_verdict(WK_BEE, false, 1800, 1800) == DV_HAND_BACK; + assert drain_verdict(WK_BEE, false, 4000, 1800) == DV_HAND_BACK; + // a bee that ended is owed nothing, at any time + assert drain_verdict(WK_BEE, true, 0, 1800) == DV_DONE; + assert drain_verdict(WK_BEE, true, 1800, 1800) == DV_DONE; + assert drain_verdict(WK_REVIEW, true, 0, 1800) == DV_DONE; + assert drain_verdict(WK_WAIT, true, 0, 1800) == DV_DONE; +} + +test a_cap_of_zero_hands_every_bee_back_at_once { + assert drain_verdict(WK_BEE, false, 0, 0) == DV_HAND_BACK; +} + +test nothing_starts_a_bee_here_and_the_rest_runs_only_while_held { + assert takes_new_work(true, 3) == false; + assert takes_new_work(true, 0) == false; + assert takes_new_work(false, 1) == true; + assert takes_new_work(false, 0) == false; +} + +test the_cap_is_todays_drain_and_fits_the_platform { + // today: runner 1800, Railway 1830 -> 1800, no bee cut sooner than before + assert drain_cap_seconds(1800, 1830) == 1800; + // a platform drain too short for the hand-back moves the cap earlier + assert drain_cap_seconds(1800, 1800) == 1775; + assert drain_cap_seconds(1800, 26) == 1; + assert drain_cap_seconds(1800, 25) == 0; + assert drain_cap_seconds(1800, 10) == 0; + // a shorter runner drain is the owner's to choose, and it stands + assert drain_cap_seconds(600, 1830) == 600; + assert drain_cap_seconds(0, 1830) == 0; + // a platform drain this process cannot read leaves the runner's drain alone + assert drain_cap_seconds(1800, 0) == 1800; +} + +test the_exit_waits_for_held_work_and_for_stops_to_land { + assert exit_step(1, 0, 0) == EXIT_WAIT; + assert exit_step(2, 3, 9000) == EXIT_WAIT; + assert exit_step(0, 0, 0) == EXIT_NOW; + assert exit_step(0, 2, 0) == EXIT_WAIT; + assert exit_step(0, 2, 4999) == EXIT_WAIT; + assert exit_step(0, 2, 5000) == EXIT_NOW; +} + +test a_handed_back_bee_is_free_at_once_and_a_left_one_waits_out_its_lease { + assert bee_free_after_seconds(true, 0) == 0; + assert bee_free_after_seconds(true, 100) == 0; + assert bee_free_after_seconds(false, 0) == 180; + assert bee_free_after_seconds(false, 45) == 135; + assert bee_free_after_seconds(false, 179) == 1; + assert bee_free_after_seconds(false, 180) == 0; + assert bee_free_after_seconds(false, 4294967295) == 0; +} + +test a_wait_row_is_free_after_the_rest_of_its_claim { + assert wait_free_after_seconds(false, 0) == 0; + assert wait_free_after_seconds(true, 0) == 60; + assert wait_free_after_seconds(true, 45) == 15; + assert wait_free_after_seconds(true, 60) == 0; +} + +invariant the_verdicts_and_kinds_are_pinned { + assert DV_DONE == 0; + assert DV_LEAVE == 1; + assert DV_STOP == 2; + assert DV_HOLD == 3; + assert DV_HAND_BACK == 4; + assert WK_BEE == 0; + assert WK_WAIT == 4; + assert WORK_KINDS == 5; + assert EXIT_NOW == 0; + assert EXIT_WAIT == 1; +} + +invariant the_literals_match_the_cards_they_come_from { + // actors.t27: turn_signal answers X_KILL at the bound and X_NONE before it + assert turn_signal(300, 300) == X_KILL_LIT; + assert turn_signal(299, 300) == X_NONE_LIT; + // control.t27: the task lease lapses at TASK_LEASE_TTL_SECONDS + assert reclaimable(false, TASK_LEASE_TTL_LIT) == true; + assert reclaimable(false, TASK_LEASE_TTL_LIT - 1) == false; + // turn_stop.t27: by LANE_FREE_MS a stopped turn's grace has passed (stop_signal says X_KILL) + assert stop_signal(LANE_FREE_MS, TURN_STOP_GRACE_MS) == X_KILL_LIT; + assert stop_signal(TURN_STOP_GRACE_MS - 1, TURN_STOP_GRACE_MS) == X_NONE_LIT; +} + +invariant a_review_is_cheap_to_redo_and_a_bee_is_not { + assert STOP_LOSS_SECONDS == REVIEW_BOUND_SECONDS; + assert redo_is_cheap(REVIEW_BOUND_SECONDS) == true; + assert redo_is_cheap(0) == false; +} + +invariant the_hand_back_fits_between_todays_drain_and_the_platform_kill { + assert HAND_BACK_SECONDS + EXIT_SECONDS <= 30; + assert drain_cap_seconds(1800, 1830) == 1800; +} diff --git a/apps/website/public/widgets/hive-drain/tool.js b/apps/website/public/widgets/hive-drain/tool.js new file mode 100644 index 0000000000..80b069990c --- /dev/null +++ b/apps/website/public/widgets/hive-drain/tool.js @@ -0,0 +1,26 @@ +// hive-drain/tool.js -- lesson 9 of queen-and-hive: what a redeploy does to running work. The cap is +// drain_cap_seconds(runner, platform) and every row is drain_verdict(kind, ended, waited, cap), fn +// bodies of gHashTag/t27 specs/queen/drain.t27 compiled to wasm (./logic.js). Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-drain.t27. +import { drainCapSeconds, drainVerdict } from './logic.js' +import { K, W, ask, controls, el, ladder, meters, root, say, source } from '../hive-kit.js' + +const ctl = controls([ + { key: 'wait', label: say('SAY_WAIT'), min: 0, max: K('K_WAIT_MAX', 2400), step: K('K_WAIT_STEP', 60) }, + { key: 'platform', label: say('SAY_PLATFORM'), min: 0, max: K('K_PLATFORM_MAX', 3600), step: K('K_PLATFORM_STEP', 30) }, + { key: 'ended', label: say('SAY_ENDED'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { wait: K('K_PRESET_WAIT', []), ended: K('K_PRESET_ENDED', []), platform: K('K_PRESET_PLATFORM', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const VERDICT_TONES = [8, 2, 4, 1, 5] + +function draw(s) { + const runner = K('K_RUNNER_DRAIN', 1800) + const cap = drainCapSeconds(runner, s.platform) + out.replaceChildren( + meters([{ label: say('SAY_WAITED'), value: Math.min(s.wait, cap), max: cap || 1, text: `${s.wait} / ${cap}`, tone: s.wait >= cap ? 5 : 1, markLabel: `${say('SAY_CAP')}: ${cap}` }]), + ladder([0, 1, 2, 3, 4].map((kind) => { const v = drainVerdict(kind, !!s.ended, s.wait, cap); return { label: `${say('SAY_KIND_NAMES', kind)}: ${say('SAY_VERDICT_NAMES', v)}`, sub: say('SAY_VERDICT_LINES', v), tone: VERDICT_TONES[v], on: true } }))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-gate/card.png b/apps/website/public/widgets/hive-gate/card.png new file mode 100644 index 0000000000..65f3550601 Binary files /dev/null and b/apps/website/public/widgets/hive-gate/card.png differ diff --git a/apps/website/public/widgets/hive-gate/hive-gate.t27 b/apps/website/public/widgets/hive-gate/hive-gate.t27 new file mode 100644 index 0000000000..2f59be49b6 --- /dev/null +++ b/apps/website/public/widgets/hive-gate/hive-gate.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-gate.t27 -- The merge gate: when an approved pull request may merge +; Source of truth for public/widgets/hive-gate/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-gate/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 12 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets how many checks posted, whether the ruleset is readable, and per +; check required, concluded, green and discounted. Each check is check_passes of its flags; the gate +; is gate_open(posted, running, blocking), the host counting the not concluded and the failing. + +; WHAT IT DOES NOT CLAIM: it does not read GitHub: the checks are what the reader taps. The merger +; itself is a workflow that meets this spec through a selftest. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_gate; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-gate"; +pub const TITLE : str = "The merge gate: when an approved pull request may merge"; +pub const DESCRIPTION : str = "Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks."; +pub const IMAGE_ALT : str = "Merge gate card: three checks posted. A required green check passes, an advisory red check that the bee discounted passes, and a required check still running blocks. The gate is shut, red: 1 running, 0 blocking."; +pub const HOOK : str = "Zero checks is not zero failures: a pull request with no posted check does not merge. And a red check counts as required whenever the ruleset cannot be read."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is check_passes and gate_open, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 12 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CHECKS : u8 = 4; +pub const K_FLAG_BITS : [4]u8 = [1, 2, 4, 8]; +pub const K_PRESET_POSTED : [4]u8 = [3, 2, 0, 2]; +pub const K_PRESET_RULESET : [4]u8 = [1, 1, 1, 0]; +pub const K_PRESET_C0 : [4]u8 = [7, 7, 0, 7]; +pub const K_PRESET_C1 : [4]u8 = [10, 10, 0, 10]; +pub const K_PRESET_C2 : [4]u8 = [1, 0, 0, 0]; +pub const K_PRESET_C3 : [4]u8 = [0, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["One check still running", "All concluded", "No check posted", "Ruleset unreadable"]; +pub const SAY_ASK : str = "When may the merger merge a pull request the reviewer bee approved?"; +pub const SAY_POSTED : str = "checks posted"; +pub const SAY_RULESET : str = "ruleset readable"; +pub const SAY_CHECK_NAMES : [4]str = ["build", "corpus ratchet", "website gate", "typecheck"]; +pub const SAY_FLAGS : [4]str = ["required", "concluded", "green", "discounted"]; +pub const SAY_PASSES : str = "passes"; +pub const SAY_BLOCKS : str = "blocks"; +pub const SAY_RUNNING : str = "running"; +pub const SAY_OPEN : str = "The gate opens: merge"; +pub const SAY_SHUT : str = "The gate stays shut"; +pub const SAY_COUNTS : str = "{0} posted, {1} running, {2} blocking"; +pub const SAY_NOTE : str = "A red check passes only when it is not required, has concluded and the bee named it as discounted. With the ruleset unreadable, every check counts as required, so no red one is discountable."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/merger_gate.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_checks_four_flags { + assert K_CHECKS == 4; + assert K_FLAG_BITS[3] == 8; +} + +test the_start_has_a_running_required_check { + assert K_PRESET_POSTED[0] == 3; + assert K_PRESET_C2[0] == 1; + assert K_PRESET_POSTED[2] == 0; +} diff --git a/apps/website/public/widgets/hive-gate/index.html b/apps/website/public/widgets/hive-gate/index.html new file mode 100644 index 0000000000..5eb4b05696 --- /dev/null +++ b/apps/website/public/widgets/hive-gate/index.html @@ -0,0 +1,47 @@ + + + + + + +The merge gate: when an approved pull request may merge · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The merge gate: when an approved pull request may merge

+

Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is check_passes and gate_open, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 12 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-gate/logic.js b/apps/website/public/widgets/hive-gate/logic.js new file mode 100644 index 0000000000..37e873827b --- /dev/null +++ b/apps/website/public/widgets/hive-gate/logic.js @@ -0,0 +1,52 @@ +// Generated by scripts/t27-logic.mjs from merger_gate.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 7390058c25153a807bd1dfc78aa132c6905779b69ab46edd6d9d383d15a74c5f +// Imported by scripts/widget-logic.mjs for public/widgets/hive-gate/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHARgA39/fwF/YAJ/fwF/YAF/AX9gBX9/f39/AX8DCQgAAQACAwEAAQQFAXABAQEFAwEAEAYJAX8BQYCAwAALB60BCQZtZW1vcnkCAA10MjdfZ2F0ZV9vcGVuAAASdDI3X2Rpc2NvdW50X2hvbGRzAAENdDI3X2xvb2tfYmFjawACDHQyN19ydW5fc2F5cwADEHQyN19jaGVja19wYXNzZXMABBl0MjdfcmVxdWlyZWRfY2hlY2tfcGFzc2VzAAUUdDI3X3JlZF9jaGVja19wYXNzZXMABhZ0MjdfY291bnRzX2FzX3JlcXVpcmVkAAcKmwEIDgAgASACckUgAEEAR3ELDgAgACABQf8BcUEBRnELIQAgAEEBQQJBACACQQJGGyACQQFGGyAAIAFBAXEbIAAbCxQAQQFBAkEAIABBAkYbIABBAUYbCyMAAkAgAkEBcUUNACADQQFxDQAgACABQQFzcSAEcSECCyACCwcAIAAgAXELDQAgAEEBcyABcSACcQsKACAAQQFzIAFyCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function countsAsRequired(rulesetReadable, required) { + return (W.t27_counts_as_required(rulesetReadable ? 1 : 0, required ? 1 : 0) & 1) !== 0; +} +function redCheckPasses(required, concluded, discounted) { + return (W.t27_red_check_passes(required ? 1 : 0, concluded ? 1 : 0, discounted ? 1 : 0) & 1) !== 0; +} +function requiredCheckPasses(posted, green) { + return (W.t27_required_check_passes(posted ? 1 : 0, green ? 1 : 0) & 1) !== 0; +} +function checkPasses(rulesetReadable, required, concluded, green, discounted) { + return (W.t27_check_passes(rulesetReadable ? 1 : 0, required ? 1 : 0, concluded ? 1 : 0, green ? 1 : 0, discounted ? 1 : 0) & 1) !== 0; +} +function runSays(run) { + return W.t27_run_says(int(run, 0, 255, "run_says.run")) & 255; +} +function lookBack(soFar, atOrAfterMergeBase, run) { + return W.t27_look_back(int(soFar, 0, 255, "look_back.so_far"), atOrAfterMergeBase ? 1 : 0, int(run, 0, 255, "look_back.run")) & 255; +} +function discountHolds(beeNamed, master) { + return (W.t27_discount_holds(beeNamed ? 1 : 0, int(master, 0, 255, "discount_holds.master")) & 1) !== 0; +} +function gateOpen(posted, running, blocking) { + return (W.t27_gate_open(int(posted, 0, 4294967295, "gate_open.posted"), int(running, 0, 4294967295, "gate_open.running"), int(blocking, 0, 4294967295, "gate_open.blocking")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + checkPasses, + countsAsRequired, + discountHolds, + gateOpen, + lookBack, + redCheckPasses, + requiredCheckPasses, + runSays +}; diff --git a/apps/website/public/widgets/hive-gate/logic.t27 b/apps/website/public/widgets/hive-gate/logic.t27 new file mode 100644 index 0000000000..cb217d2ebb --- /dev/null +++ b/apps/website/public/widgets/hive-gate/logic.t27 @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved +; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq). +; It does not import this spec, and t27c gen-js lowers no function bodies (#7300). +; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario +; asserts the verdict that scenario pins for the workflow. +; Negative controls: discounted required check, unconcluded check, required check never posted, +; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run +; of the same check was CANCELLED (#7317) +; ASCII only (L3) +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenMergerGate; + +/// With the ruleset unreadable the merger cannot tell which checks are required, +/// so every check counts as required and no red one is discountable (#5776 item 2). +pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool { + return !ruleset_readable || required; +} + +/// A red check passes only when it is not required, has concluded, and is discounted +pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool { + return !required && concluded && discounted; +} + +/// A required check passes when it is posted and green +pub fn required_check_passes(posted: bool, green: bool) -> bool { + return posted && green; +} + +/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks; +/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check +/// that does not count as required. +pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool { + if (!concluded) { return false; } + if (green) { return true; } + return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted); +} + +// --------------------------------------------------------------------------- +// What master says about a discounted red (#7317) +// +// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its +// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled +// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it. +// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the +// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or +// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts. + +/// One master run of the red check, as the merger reads its job's status and conclusion. +/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code. +pub const RUN_MISSING : u8 = 0; +pub const RUN_FAILURE : u8 = 1; +pub const RUN_SUCCESS : u8 = 2; +pub const RUN_CANCELLED : u8 = 3; +pub const RUN_SKIPPED : u8 = 4; +pub const RUN_TIMED_OUT : u8 = 5; +/// queued or in progress +pub const RUN_RUNNING : u8 = 6; +/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail +pub const RUN_OTHER : u8 = 7; + +/// What master says about the PR's red. +pub const MASTER_UNKNOWN : u8 = 0; +/// a completed failure: the red is inherited +pub const MASTER_RED : u8 = 1; +/// a completed success: master was green, the red is the PR's own +pub const MASTER_GREEN : u8 = 2; + +/// One run on its own. Only a completed failure or a completed success decides anything. +pub fn run_says(run: u8) -> u8 { + if (run == RUN_FAILURE) { return MASTER_RED; } + if (run == RUN_SUCCESS) { return MASTER_GREEN; } + return MASTER_UNKNOWN; +} + +/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN. +/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not. +/// A run older than the PR's merge base is not evidence about the code the PR started from. +pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 { + if (so_far != MASTER_UNKNOWN) { return so_far; } + if (!at_or_after_merge_base) { return MASTER_UNKNOWN; } + return run_says(run); +} + +/// A red is discounted only when the bee's approval names it AND master says red. +/// This is the `discounted` that check_passes takes. +pub fn discount_holds(bee_named: bool, master: u8) -> bool { + return bee_named && master == MASTER_RED; +} + +/// The gate opens when at least one check has posted, none is still running, +/// and none blocks. Zero checks is not zero failures. +pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool { + return posted > 0 && running == 0 && blocking == 0; +} + +// Tests for counts_as_required + +test counts_as_required_follows_the_ruleset_when_readable { + assert counts_as_required(true, true) == true; + assert counts_as_required(true, false) == false; +} + +test counts_as_required_is_every_check_when_unreadable { + assert counts_as_required(false, false) == true; + assert counts_as_required(false, true) == true; +} + +// Tests for red_check_passes + +// Positive case: non-required check that concluded and is discounted +test red_check_passes_valid_discounted_non_required { + assert red_check_passes(false, true, true) == true; +} + +// Negative control: discounted required check (required=true, cannot be discounted) +test red_check_passes_discounted_required_fails { + assert red_check_passes(true, true, true) == false; +} + +// Negative control: unconcluded check (concluded=false, even if discounted) +test red_check_passes_unconcluded_fails { + assert red_check_passes(false, false, true) == false; +} + +// Negative control: non-required check that concluded but not discounted +test red_check_passes_non_discounted_fails { + assert red_check_passes(false, true, false) == false; +} + +test red_check_passes_all_false_fails { + assert red_check_passes(true, false, false) == false; +} + +// Tests for required_check_passes + +test required_check_passes_posted_and_green_passes { + assert required_check_passes(true, true) == true; +} + +// Self-test scenario "required check never posted" +test required_check_passes_not_posted_fails { + assert required_check_passes(false, true) == false; +} + +test required_check_passes_posted_but_not_green_fails { + assert required_check_passes(true, false) == false; +} + +test required_check_passes_neither_posted_nor_green_fails { + assert required_check_passes(false, false) == false; +} + +// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted) + +test check_passes_green_advisory_and_green_required_pass { + assert check_passes(true, false, true, true, false) == true; + assert check_passes(true, true, true, true, false) == true; +} + +// Self-test scenario "red advisory check, discounted in the bee's approval" +test check_passes_red_advisory_discounted_passes { + assert check_passes(true, false, true, false, true) == true; +} + +// Self-test scenario "red advisory check, not discounted" +test check_passes_red_advisory_not_discounted_blocks { + assert check_passes(true, false, true, false, false) == false; +} + +// Self-test scenario "red REQUIRED check, discounted anyway" +test check_passes_red_required_discounted_blocks { + assert check_passes(true, true, true, false, true) == false; +} + +// Self-test scenarios "a check still running" and "pending commit status blocks": +// not concluded blocks whatever else is true, a discount included +test check_passes_running_check_blocks { + assert check_passes(true, false, false, true, true) == false; + assert check_passes(true, false, false, false, true) == false; +} + +// Self-test scenario "ruleset unreadable: a discounted red check still blocks" +test check_passes_unreadable_ruleset_discounted_red_blocks { + assert check_passes(false, false, true, false, true) == false; +} + +// Self-test scenario "ruleset unreadable, every check green" +test check_passes_unreadable_ruleset_green_passes { + assert check_passes(false, false, true, true, false) == true; +} + +// Tests for gate_open: (posted, running, blocking) + +// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks +test gate_open_posted_checks_all_pass_opens { + assert gate_open(2, 0, 0) == true; + assert gate_open(1, 0, 0) == true; +} + +// Negative control: zero checks is not zero failures +test gate_open_zero_checks_stays_closed { + assert gate_open(0, 0, 0) == false; +} + +// Self-test scenario "a check still running" +test gate_open_running_check_keeps_it_closed { + assert gate_open(3, 1, 0) == false; +} + +// Self-test scenario "two red checks, only one discounted" +test gate_open_one_blocking_check_keeps_it_closed { + assert gate_open(4, 0, 1) == false; +} + +// Tests for run_says, look_back and discount_holds (#7317) + +test only_a_completed_failure_says_red { + assert run_says(RUN_FAILURE) == MASTER_RED; + assert run_says(RUN_SUCCESS) == MASTER_GREEN; +} + +test every_other_run_says_unknown { + assert run_says(RUN_MISSING) == MASTER_UNKNOWN; + assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN; + assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN; + assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN; + assert run_says(RUN_RUNNING) == MASTER_UNKNOWN; + assert run_says(RUN_OTHER) == MASTER_UNKNOWN; +} + +// Self-test scenario "master newest cancelled, older failure at or after the merge base" +test look_back_passes_a_cancelled_run_to_an_older_failure { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED; +} + +// Self-test scenario "master failure only before the merge base" +test look_back_stops_at_the_merge_base { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN; + assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN; +} + +// The newest decisive run wins, in both directions +test look_back_keeps_the_newest_decisive_run { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN; + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED; + assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED; +} + +test look_back_over_only_unknown_runs_stays_unknown { + assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN; +} + +// Self-test scenario "master failure": discounted +test discount_holds_on_a_master_failure { + assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true; + assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true; +} + +// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked +test pr_red_while_master_cancelled_is_blocked { + assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false; + assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false; +} + +// Self-test scenario "master success": the red is the PR's own -> blocked +test pr_red_while_master_green_is_blocked { + assert discount_holds(true, MASTER_GREEN) == false; + assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false; +} + +// Master red, but the bee never named the check: blocked +test master_red_without_the_bees_line_is_blocked { + assert discount_holds(false, MASTER_RED) == false; + assert discount_holds(false, MASTER_UNKNOWN) == false; +} + +invariant the_run_and_master_codes_never_move { + assert RUN_MISSING == 0; + assert RUN_FAILURE == 1; + assert RUN_SUCCESS == 2; + assert RUN_CANCELLED == 3; + assert RUN_SKIPPED == 4; + assert RUN_TIMED_OUT == 5; + assert RUN_RUNNING == 6; + assert RUN_OTHER == 7; + assert MASTER_UNKNOWN == 0; + assert MASTER_RED == 1; + assert MASTER_GREEN == 2; +} + +invariant unknown_never_discounts { + assert discount_holds(true, MASTER_UNKNOWN) == false; + assert discount_holds(true, run_says(RUN_MISSING)) == false; + assert discount_holds(true, run_says(RUN_CANCELLED)) == false; + assert discount_holds(true, run_says(RUN_SKIPPED)) == false; + assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false; + assert discount_holds(true, run_says(RUN_RUNNING)) == false; + assert discount_holds(true, run_says(RUN_OTHER)) == false; + assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false; +} diff --git a/apps/website/public/widgets/hive-gate/tool.js b/apps/website/public/widgets/hive-gate/tool.js new file mode 100644 index 0000000000..a193c80a3b --- /dev/null +++ b/apps/website/public/widgets/hive-gate/tool.js @@ -0,0 +1,36 @@ +// hive-gate/tool.js -- lesson 12 of queen-and-hive: when the merger may merge an approved pull +// request. Each check is check_passes of its flags and the gate is gate_open(posted, running, +// blocking), fn bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm (./logic.js); the +// host counts the checks not concluded and the concluded ones that do not pass. Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-gate.t27. +import { checkPasses, gateOpen } from './logic.js' +import { K, W, ask, controls, el, fill, gate, pill, root, say, source, taps } from '../hive-kit.js' + +const N = K('K_CHECKS', 4), BITS = K('K_FLAG_BITS', [1, 2, 4, 8]) +const keys = [...Array(N)].map((_, i) => `c${i}`) +const ctl = controls([ + { key: 'posted', label: say('SAY_POSTED'), min: 0, max: N, step: 1 }, + { key: 'ruleset', label: say('SAY_RULESET'), kind: 'toggle' }, + ...keys.map((key) => ({ key, kind: 'hidden', min: 0, max: 15 })), +], { names: W.SAY_PRESET_NAMES, values: { posted: K('K_PRESET_POSTED', []), ruleset: K('K_PRESET_RULESET', []), ...Object.fromEntries(keys.map((k, i) => [k, K(`K_PRESET_C${i}`, [])])) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + let running = 0, blocking = 0 + const rows = keys.slice(0, s.posted).map((k, i) => { + const f = (j) => !!(s[k] & BITS[j]) + const concluded = f(1) + const pass = checkPasses(!!s.ruleset, f(0), concluded, f(2), f(3)) + if (!concluded) running++ + else if (!pass) blocking++ + return el('div', { class: 'hk-path' }, + el('p', { class: 'hk-path-head' }, el('code', {}, say('SAY_CHECK_NAMES', i)), pill(!concluded ? say('SAY_RUNNING') : pass ? say('SAY_PASSES') : say('SAY_BLOCKS'), !concluded ? 1 : pass ? 0 : 6)), + taps(BITS.map((b, j) => ({ label: say('SAY_FLAGS', j), pressed: !!(s[k] & b), tone: !concluded ? 1 : pass ? 0 : 6 })), (j) => ctl.set(k, s[k] ^ BITS[j]), say('SAY_CHECK_NAMES', i))) + }) + const open = gateOpen(s.posted, running, blocking) + out.replaceChildren(...rows, gate(open, open ? say('SAY_OPEN') : say('SAY_SHUT'), fill(say('SAY_COUNTS'), s.posted, running, blocking))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-jam-action/card.png b/apps/website/public/widgets/hive-jam-action/card.png new file mode 100644 index 0000000000..4314172bd9 Binary files /dev/null and b/apps/website/public/widgets/hive-jam-action/card.png differ diff --git a/apps/website/public/widgets/hive-jam-action/hive-jam-action.t27 b/apps/website/public/widgets/hive-jam-action/hive-jam-action.t27 new file mode 100644 index 0000000000..e33ab63464 --- /dev/null +++ b/apps/website/public/widgets/hive-jam-action/hive-jam-action.t27 @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-jam-action.t27 -- The Queen's move: what each jam asks of her +; Source of truth for public/widgets/hive-jam-action/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-jam-action/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/review-jam.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 14 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the case from 0 to 255 or taps a preset. The verdict is +; jam_action(case); each of the ten case cards sits in the column jam_is_queens_turn of its own +; case. + +; WHAT IT DOES NOT CLAIM: it does not act: the page shows the move, it does not dispatch a bee. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_jam_action; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-jam-action"; +pub const TITLE : str = "The Queen's move: what each jam asks of her"; +pub const DESCRIPTION : str = "Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting."; +pub const IMAGE_ALT : str = "Jam action card: case 4, refused then pushed. The action reads dispatch a re-review. Ten case cards stand on two columns: seven in the gold QUEEN'S TURN column, none, draft or fresh and stacked on open in the grey WAIT column; case 4 is outlined."; +pub const HOOK : str = "An unknown case is never nothing to do: anything above 9 maps to measure again. A queue that cannot be read is not reported as free."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_action and jam_is_queens_turn, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 14 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CASES : u8 = 10; +pub const K_CASE_MAX : u8 = 255; +pub const K_PRESET_CASE : [5]u8 = [4, 1, 8, 6, 200]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Refused then pushed", "No reviewer", "Draft or fresh", "Conflicting", "Unknown case 200"]; +pub const SAY_ASK : str = "Which move is the Queen's for each jam, and which jams are not hers to move?"; +pub const SAY_CASE : str = "case"; +pub const SAY_CASE_NAMES : [10]str = ["none", "no reviewer", "stacked on open", "refused not fixed", "refused then pushed", "labeled not merged", "conflicting", "noise red", "draft or fresh", "unmeasured"]; +pub const SAY_ACTION_NAMES : [10]str = ["nothing", "dispatch a reviewer", "wait for the base", "dispatch the coder", "dispatch a re-review", "finish the merge", "rebase, by the coder", "name the noise and review", "wait", "measure again"]; +pub const SAY_COLUMNS : [2]str = ["QUEEN'S TURN", "WAIT"]; +pub const SAY_UNKNOWN : str = "unknown case {0}"; +pub const SAY_ACTION : str = "action"; +pub const SAY_NOTE : str = "A draft, a fresh PR and one stacked on an open PR are waited on, not pushed. Every other jam is a move the Queen makes herself, in the order of the board."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/review-jam.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_covers_unknown_cases { + assert K_CASE_MAX > K_CASES; + assert K_PRESET_CASE[4] > K_CASES; +} + +test the_start_is_refused_then_pushed { + assert K_PRESET_CASE[0] == 4; + assert K_PRESET_CASE[0] < K_CASES; +} diff --git a/apps/website/public/widgets/hive-jam-action/index.html b/apps/website/public/widgets/hive-jam-action/index.html new file mode 100644 index 0000000000..5a1998295f --- /dev/null +++ b/apps/website/public/widgets/hive-jam-action/index.html @@ -0,0 +1,47 @@ + + + + + + +The Queen's move: what each jam asks of her · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The Queen's move: what each jam asks of her

+

Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_action and jam_is_queens_turn, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 14 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-jam-action/logic.js b/apps/website/public/widgets/hive-jam-action/logic.js new file mode 100644 index 0000000000..c8b5f741d5 --- /dev/null +++ b/apps/website/public/widgets/hive-jam-action/logic.js @@ -0,0 +1,32 @@ +// Generated by scripts/t27-logic.mjs from review-jam.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 4cc0c0e1fa6d6abd3c9b8a7658f8a2d31a8fba24bb7ad4efcf60bf194497a662 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-jam-action/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABEwJgAX8Bf2AJf39/f39/f39/AX8DBAMAAAEEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdDBAZtZW1vcnkCABZ0MjdfamFtX2lzX3F1ZWVuc190dXJuAAAOdDI3X2phbV9hY3Rpb24AAQx0MjdfamFtX2Nhc2UAAgqQAQMOACAAQQhLQfoBIAB2cgsMACAAQQkgAEEJSRsLcgBBCEECIAFBAXEbQQkgAEEBcRtBBkEFIANBAXEiAxsgAEF/cyABciACckEBcSIAGyEBAkAgAA0AIAMNACAEQQFxDQBBBEEDIAZBAXEbQQcgBUEBcSIAGyEBIAANACAIQQFxDQAgB0F/c0EBcSEBCyABCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function jamCase(measured, draftOrFresh, stackedOnOpen, conflicting, labeledReviewed, refused, pushedAfterRefusal, hasReviewer, onlyNoiseRed) { + return W.t27_jam_case(measured ? 1 : 0, draftOrFresh ? 1 : 0, stackedOnOpen ? 1 : 0, conflicting ? 1 : 0, labeledReviewed ? 1 : 0, refused ? 1 : 0, pushedAfterRefusal ? 1 : 0, hasReviewer ? 1 : 0, onlyNoiseRed ? 1 : 0) & 255; +} +function jamAction(case_) { + return W.t27_jam_action(int(case_, 0, 255, "jam_action.case")) & 255; +} +function jamIsQueensTurn(case_) { + return (W.t27_jam_is_queens_turn(int(case_, 0, 255, "jam_is_queens_turn.case")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + jamAction, + jamCase, + jamIsQueensTurn +}; diff --git a/apps/website/public/widgets/hive-jam-action/logic.t27 b/apps/website/public/widgets/hive-jam-action/logic.t27 new file mode 100644 index 0000000000..80cd798a66 --- /dev/null +++ b/apps/website/public/widgets/hive-jam-action/logic.t27 @@ -0,0 +1,274 @@ +// Review jam: which case a stuck pull request is in, and the Queen's action. +// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700). +// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead +// of hand-copying them. + +module review_jam; + +// Jam case constants (u8, value = position, starting at 0) +pub const JAM_NONE: u8 = 0; +pub const JAM_NO_REVIEWER: u8 = 1; +pub const JAM_STACKED_ON_OPEN: u8 = 2; +pub const JAM_REFUSED_NOT_FIXED: u8 = 3; +pub const JAM_REFUSED_THEN_PUSHED: u8 = 4; +pub const JAM_LABELED_NOT_MERGED: u8 = 5; +pub const JAM_CONFLICTING: u8 = 6; +pub const JAM_NOISE_RED: u8 = 7; +pub const JAM_DRAFT_OR_FRESH: u8 = 8; +pub const JAM_UNMEASURED: u8 = 9; + +// Queen action constants (u8, value = position, starting at 0) +pub const ACT_NOTHING: u8 = 0; +pub const ACT_DISPATCH_REVIEWER: u8 = 1; +pub const ACT_WAIT_FOR_BASE: u8 = 2; +pub const ACT_DISPATCH_CODER: u8 = 3; +pub const ACT_DISPATCH_RE_REVIEW: u8 = 4; +pub const ACT_FINISH_MERGE: u8 = 5; +pub const ACT_REBASE_BY_CODER: u8 = 6; +pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7; +pub const ACT_WAIT: u8 = 8; +pub const ACT_MEASURE_AGAIN: u8 = 9; + +// The jam case of a pull request. The FIRST matching rule wins: +// 1. not measured -> UNMEASURED +// 2. draft or fresh -> DRAFT_OR_FRESH +// 3. stacked on an open PR -> STACKED_ON_OPEN +// 4. conflicting -> CONFLICTING +// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED +// 6. refused and pushed after -> REFUSED_THEN_PUSHED +// 7. refused -> REFUSED_NOT_FIXED +// 8. only noise checks red -> NOISE_RED +// 9. no reviewer -> NO_REVIEWER +// 10. otherwise -> NONE +pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 { + if !measured { + return JAM_UNMEASURED; + } + if draft_or_fresh { + return JAM_DRAFT_OR_FRESH; + } + if stacked_on_open { + return JAM_STACKED_ON_OPEN; + } + if conflicting { + return JAM_CONFLICTING; + } + if labeled_reviewed { + return JAM_LABELED_NOT_MERGED; + } + if refused && pushed_after_refusal { + return JAM_REFUSED_THEN_PUSHED; + } + if refused { + return JAM_REFUSED_NOT_FIXED; + } + if only_noise_red { + return JAM_NOISE_RED; + } + if !has_reviewer { + return JAM_NO_REVIEWER; + } + JAM_NONE +} + +// The Queen's action for a case: the action with the SAME position. +// Any value above 9 is an unknown case, and an unknown case is never +// "nothing to do": it maps to ACT_MEASURE_AGAIN. +pub fn jam_action(case: u8) -> u8 { + if case > 9 { + return ACT_MEASURE_AGAIN; + } + case +} + +// Whether the case is the Queen's turn to act: true for every case except +// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN. +pub fn jam_is_queens_turn(case: u8) -> bool { + case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN +} + +// ---- jam_case: one test per case ---- + +test "unmeasured beats every other flag" { + assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED; +} + +test "unmeasured with every other flag clear" { + assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED; +} + +test "draft or fresh" { + assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH; +} + +test "stacked on open PR" { + assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN; +} + +test "conflicting" { + assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING; +} + +test "labeled not merged" { + assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED; +} + +test "refused then pushed" { + assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED; +} + +test "refused not fixed" { + assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED; +} + +test "noise red" { + assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED; +} + +test "no reviewer" { + assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER; +} + +test "none" { + assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE; +} + +// ---- jam_case: first-match order, each rule against the next one ---- + +test "draft beats stacked" { + assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH; +} + +test "stacked and unreviewed is stacked, not no reviewer" { + assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN; +} + +test "stacked beats conflicting" { + assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN; +} + +test "conflicting beats labeled" { + assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING; +} + +test "labeled beats refused then pushed" { + assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED; +} + +test "refused then pushed beats noise red" { + assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED; +} + +test "refused not fixed beats noise red" { + assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED; +} + +test "noise red beats no reviewer" { + assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED; +} + +test "pushed without a refusal is not refused then pushed" { + assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE; +} + +// ---- jam_action: same position, unknown -> measure again ---- + +test "action none to nothing" { + assert jam_action(JAM_NONE) == ACT_NOTHING; +} + +test "action no reviewer to dispatch reviewer" { + assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER; +} + +test "action stacked to wait for base" { + assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE; +} + +test "action refused not fixed to dispatch coder" { + assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER; +} + +test "action refused then pushed to dispatch re-review" { + assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW; +} + +test "action labeled not merged to finish merge" { + assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE; +} + +test "action conflicting to rebase by coder" { + assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER; +} + +test "action noise red to name noise and review" { + assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW; +} + +test "action draft or fresh to wait" { + assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT; +} + +test "action unmeasured to measure again" { + assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN; +} + +test "action first unknown case measures again" { + assert jam_action(10) == ACT_MEASURE_AGAIN; +} + +test "action unknown case 200 measures again" { + assert jam_action(200) == ACT_MEASURE_AGAIN; +} + +// ---- jam_is_queens_turn ---- + +test "queens turn for no reviewer" { + assert jam_is_queens_turn(JAM_NO_REVIEWER) == true; +} + +test "queens turn for refused not fixed" { + assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true; +} + +test "queens turn for refused then pushed" { + assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true; +} + +test "queens turn for labeled not merged" { + assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true; +} + +test "queens turn for conflicting" { + assert jam_is_queens_turn(JAM_CONFLICTING) == true; +} + +test "queens turn for noise red" { + assert jam_is_queens_turn(JAM_NOISE_RED) == true; +} + +test "queens turn for unmeasured" { + assert jam_is_queens_turn(JAM_UNMEASURED) == true; +} + +test "not queens turn for none" { + assert jam_is_queens_turn(JAM_NONE) == false; +} + +test "not queens turn for draft or fresh" { + assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false; +} + +test "not queens turn for stacked on open" { + assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false; +} + +// ---- the two scenarios of #9032, end to end ---- + +test "scenario bee-reviewed but still open finishes the merge" { + assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE; +} + +test "scenario failed read is never reported as free" { + assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN; +} diff --git a/apps/website/public/widgets/hive-jam-action/tool.js b/apps/website/public/widgets/hive-jam-action/tool.js new file mode 100644 index 0000000000..4b671d0848 --- /dev/null +++ b/apps/website/public/widgets/hive-jam-action/tool.js @@ -0,0 +1,22 @@ +// hive-jam-action/tool.js -- lesson 14 of queen-and-hive: the Queen's move for each jam. The action is +// jam_action(case) and each case card's column is jam_is_queens_turn of its case, fn bodies of +// gHashTag/t27 specs/automation/review-jam.t27 compiled to wasm (./logic.js). Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-jam-action.t27. +import { jamAction, jamIsQueensTurn } from './logic.js' +import { K, W, ask, board, controls, el, fill, root, say, source, verdict } from '../hive-kit.js' + +const NC = K('K_CASES', 10) +const ctl = controls([{ key: 'c', label: say('SAY_CASE'), min: 0, max: K('K_CASE_MAX', 255), step: 1 }], { names: W.SAY_PRESET_NAMES, values: { c: K('K_PRESET_CASE', []) } }, draw) +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const a = jamAction(s.c) + const name = s.c < NC ? say('SAY_CASE_NAMES', s.c) : fill(say('SAY_UNKNOWN'), s.c) + out.replaceChildren( + verdict(`${say('SAY_ACTION')}: ${say('SAY_ACTION_NAMES', a)}`, jamIsQueensTurn(s.c) ? 1 : 8, `${say('SAY_CASE')} ${s.c}: ${name}`), + board([{ name: say('SAY_COLUMNS', 0), tone: 1 }, { name: say('SAY_COLUMNS', 1), tone: 8 }], + [...Array(NC)].map((_, k) => ({ col: jamIsQueensTurn(k) ? 0 : 1, title: `${k}. ${say('SAY_CASE_NAMES', k)}`, sub: say('SAY_ACTION_NAMES', jamAction(k)), tone: jamIsQueensTurn(k) ? 1 : 8, on: k === s.c })))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-jam/card.png b/apps/website/public/widgets/hive-jam/card.png new file mode 100644 index 0000000000..8c14a89b31 Binary files /dev/null and b/apps/website/public/widgets/hive-jam/card.png differ diff --git a/apps/website/public/widgets/hive-jam/hive-jam.t27 b/apps/website/public/widgets/hive-jam/hive-jam.t27 new file mode 100644 index 0000000000..dfc818857d --- /dev/null +++ b/apps/website/public/widgets/hive-jam/hive-jam.t27 @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-jam.t27 -- The review jam: which case a stuck pull request is in +; Source of truth for public/widgets/hive-jam/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-jam/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/review-jam.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 13 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader toggles the nine facts. The lit rung, where the pull request sits, is +; the case jam_case(measured, draft or fresh, stacked, conflicting, labeled, refused, pushed after, +; has reviewer, only noise red) names. + +; WHAT IT DOES NOT CLAIM: it reads no pull request: the facts are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_jam; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-jam"; +pub const TITLE : str = "The review jam: which case a stuck pull request is in"; +pub const DESCRIPTION : str = "Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule."; +pub const IMAGE_ALT : str = "Review jam card: a measured pull request, refused, pushed after the refusal, with a reviewer and only noise checks red. Of ten rules in first-match order the sixth, refused then pushed, is lit and holds the card, ahead of noise red, which also matches."; +pub const HOOK : str = "A pull request can be refused, pushed again and red at once. The earliest rule that matches wins, so the Queen always knows which one case she is in."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_case, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 13 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CASES : u8 = 10; +; The case jam_case returns when no rule matches; the rule order is read from jam_case itself (tool.js). +pub const K_JAM_NONE : u8 = 0; +pub const K_PRESET_FLAGS : [5]u16 = [481, 0, 3, 9, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Refused, pushed, noise red", "Not measured", "A draft", "Conflicting", "No reviewer yet"]; +pub const SAY_ASK : str = "A pull request has not moved for hours. Which jam is it in?"; +pub const SAY_FLAGS : [9]str = ["measured", "draft or fresh", "stacked on an open PR", "conflicting", "labeled reviewed", "refused", "pushed after the refusal", "has a reviewer", "only noise checks red"]; +pub const SAY_CASE_NAMES : [10]str = ["NONE", "NO REVIEWER", "STACKED ON OPEN", "REFUSED NOT FIXED", "REFUSED THEN PUSHED", "LABELED NOT MERGED", "CONFLICTING", "NOISE RED", "DRAFT OR FRESH", "UNMEASURED"]; +pub const SAY_RULES : str = "the rules, first match wins"; +pub const SAY_PR : str = "this pull request"; +pub const SAY_CASE : str = "case"; +pub const SAY_NOTE : str = "Pushed without a refusal is not refused then pushed: it is no jam at all. Unmeasured beats every other fact, because a pull request the Queen could not read is never reported as free."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/review-jam.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test ten_cases_ten_rules { + assert K_CASES == 10; + assert K_JAM_NONE == 0; + assert K_JAM_NONE < K_CASES; +} + +test the_start_is_measured_and_refused { + assert K_PRESET_FLAGS[0] == 481; + assert K_PRESET_FLAGS[1] == 0; +} diff --git a/apps/website/public/widgets/hive-jam/index.html b/apps/website/public/widgets/hive-jam/index.html new file mode 100644 index 0000000000..66a145eee3 --- /dev/null +++ b/apps/website/public/widgets/hive-jam/index.html @@ -0,0 +1,47 @@ + + + + + + +The review jam: which case a stuck pull request is in · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The review jam: which case a stuck pull request is in

+

Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_case, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 13 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-jam/logic.js b/apps/website/public/widgets/hive-jam/logic.js new file mode 100644 index 0000000000..4d0b3b08b4 --- /dev/null +++ b/apps/website/public/widgets/hive-jam/logic.js @@ -0,0 +1,32 @@ +// Generated by scripts/t27-logic.mjs from review-jam.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 4cc0c0e1fa6d6abd3c9b8a7658f8a2d31a8fba24bb7ad4efcf60bf194497a662 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-jam/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABEwJgAX8Bf2AJf39/f39/f39/AX8DBAMAAAEEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdDBAZtZW1vcnkCABZ0MjdfamFtX2lzX3F1ZWVuc190dXJuAAAOdDI3X2phbV9hY3Rpb24AAQx0MjdfamFtX2Nhc2UAAgqQAQMOACAAQQhLQfoBIAB2cgsMACAAQQkgAEEJSRsLcgBBCEECIAFBAXEbQQkgAEEBcRtBBkEFIANBAXEiAxsgAEF/cyABciACckEBcSIAGyEBAkAgAA0AIAMNACAEQQFxDQBBBEEDIAZBAXEbQQcgBUEBcSIAGyEBIAANACAIQQFxDQAgB0F/c0EBcSEBCyABCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function jamCase(measured, draftOrFresh, stackedOnOpen, conflicting, labeledReviewed, refused, pushedAfterRefusal, hasReviewer, onlyNoiseRed) { + return W.t27_jam_case(measured ? 1 : 0, draftOrFresh ? 1 : 0, stackedOnOpen ? 1 : 0, conflicting ? 1 : 0, labeledReviewed ? 1 : 0, refused ? 1 : 0, pushedAfterRefusal ? 1 : 0, hasReviewer ? 1 : 0, onlyNoiseRed ? 1 : 0) & 255; +} +function jamAction(case_) { + return W.t27_jam_action(int(case_, 0, 255, "jam_action.case")) & 255; +} +function jamIsQueensTurn(case_) { + return (W.t27_jam_is_queens_turn(int(case_, 0, 255, "jam_is_queens_turn.case")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + jamAction, + jamCase, + jamIsQueensTurn +}; diff --git a/apps/website/public/widgets/hive-jam/logic.t27 b/apps/website/public/widgets/hive-jam/logic.t27 new file mode 100644 index 0000000000..80cd798a66 --- /dev/null +++ b/apps/website/public/widgets/hive-jam/logic.t27 @@ -0,0 +1,274 @@ +// Review jam: which case a stuck pull request is in, and the Queen's action. +// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700). +// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead +// of hand-copying them. + +module review_jam; + +// Jam case constants (u8, value = position, starting at 0) +pub const JAM_NONE: u8 = 0; +pub const JAM_NO_REVIEWER: u8 = 1; +pub const JAM_STACKED_ON_OPEN: u8 = 2; +pub const JAM_REFUSED_NOT_FIXED: u8 = 3; +pub const JAM_REFUSED_THEN_PUSHED: u8 = 4; +pub const JAM_LABELED_NOT_MERGED: u8 = 5; +pub const JAM_CONFLICTING: u8 = 6; +pub const JAM_NOISE_RED: u8 = 7; +pub const JAM_DRAFT_OR_FRESH: u8 = 8; +pub const JAM_UNMEASURED: u8 = 9; + +// Queen action constants (u8, value = position, starting at 0) +pub const ACT_NOTHING: u8 = 0; +pub const ACT_DISPATCH_REVIEWER: u8 = 1; +pub const ACT_WAIT_FOR_BASE: u8 = 2; +pub const ACT_DISPATCH_CODER: u8 = 3; +pub const ACT_DISPATCH_RE_REVIEW: u8 = 4; +pub const ACT_FINISH_MERGE: u8 = 5; +pub const ACT_REBASE_BY_CODER: u8 = 6; +pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7; +pub const ACT_WAIT: u8 = 8; +pub const ACT_MEASURE_AGAIN: u8 = 9; + +// The jam case of a pull request. The FIRST matching rule wins: +// 1. not measured -> UNMEASURED +// 2. draft or fresh -> DRAFT_OR_FRESH +// 3. stacked on an open PR -> STACKED_ON_OPEN +// 4. conflicting -> CONFLICTING +// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED +// 6. refused and pushed after -> REFUSED_THEN_PUSHED +// 7. refused -> REFUSED_NOT_FIXED +// 8. only noise checks red -> NOISE_RED +// 9. no reviewer -> NO_REVIEWER +// 10. otherwise -> NONE +pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 { + if !measured { + return JAM_UNMEASURED; + } + if draft_or_fresh { + return JAM_DRAFT_OR_FRESH; + } + if stacked_on_open { + return JAM_STACKED_ON_OPEN; + } + if conflicting { + return JAM_CONFLICTING; + } + if labeled_reviewed { + return JAM_LABELED_NOT_MERGED; + } + if refused && pushed_after_refusal { + return JAM_REFUSED_THEN_PUSHED; + } + if refused { + return JAM_REFUSED_NOT_FIXED; + } + if only_noise_red { + return JAM_NOISE_RED; + } + if !has_reviewer { + return JAM_NO_REVIEWER; + } + JAM_NONE +} + +// The Queen's action for a case: the action with the SAME position. +// Any value above 9 is an unknown case, and an unknown case is never +// "nothing to do": it maps to ACT_MEASURE_AGAIN. +pub fn jam_action(case: u8) -> u8 { + if case > 9 { + return ACT_MEASURE_AGAIN; + } + case +} + +// Whether the case is the Queen's turn to act: true for every case except +// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN. +pub fn jam_is_queens_turn(case: u8) -> bool { + case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN +} + +// ---- jam_case: one test per case ---- + +test "unmeasured beats every other flag" { + assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED; +} + +test "unmeasured with every other flag clear" { + assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED; +} + +test "draft or fresh" { + assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH; +} + +test "stacked on open PR" { + assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN; +} + +test "conflicting" { + assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING; +} + +test "labeled not merged" { + assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED; +} + +test "refused then pushed" { + assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED; +} + +test "refused not fixed" { + assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED; +} + +test "noise red" { + assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED; +} + +test "no reviewer" { + assert jam_case(true, false, false, false, false, false, false, false, false) == JAM_NO_REVIEWER; +} + +test "none" { + assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE; +} + +// ---- jam_case: first-match order, each rule against the next one ---- + +test "draft beats stacked" { + assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH; +} + +test "stacked and unreviewed is stacked, not no reviewer" { + assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN; +} + +test "stacked beats conflicting" { + assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN; +} + +test "conflicting beats labeled" { + assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING; +} + +test "labeled beats refused then pushed" { + assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED; +} + +test "refused then pushed beats noise red" { + assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED; +} + +test "refused not fixed beats noise red" { + assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED; +} + +test "noise red beats no reviewer" { + assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED; +} + +test "pushed without a refusal is not refused then pushed" { + assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE; +} + +// ---- jam_action: same position, unknown -> measure again ---- + +test "action none to nothing" { + assert jam_action(JAM_NONE) == ACT_NOTHING; +} + +test "action no reviewer to dispatch reviewer" { + assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER; +} + +test "action stacked to wait for base" { + assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE; +} + +test "action refused not fixed to dispatch coder" { + assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER; +} + +test "action refused then pushed to dispatch re-review" { + assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW; +} + +test "action labeled not merged to finish merge" { + assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE; +} + +test "action conflicting to rebase by coder" { + assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER; +} + +test "action noise red to name noise and review" { + assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW; +} + +test "action draft or fresh to wait" { + assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT; +} + +test "action unmeasured to measure again" { + assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN; +} + +test "action first unknown case measures again" { + assert jam_action(10) == ACT_MEASURE_AGAIN; +} + +test "action unknown case 200 measures again" { + assert jam_action(200) == ACT_MEASURE_AGAIN; +} + +// ---- jam_is_queens_turn ---- + +test "queens turn for no reviewer" { + assert jam_is_queens_turn(JAM_NO_REVIEWER) == true; +} + +test "queens turn for refused not fixed" { + assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true; +} + +test "queens turn for refused then pushed" { + assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true; +} + +test "queens turn for labeled not merged" { + assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true; +} + +test "queens turn for conflicting" { + assert jam_is_queens_turn(JAM_CONFLICTING) == true; +} + +test "queens turn for noise red" { + assert jam_is_queens_turn(JAM_NOISE_RED) == true; +} + +test "queens turn for unmeasured" { + assert jam_is_queens_turn(JAM_UNMEASURED) == true; +} + +test "not queens turn for none" { + assert jam_is_queens_turn(JAM_NONE) == false; +} + +test "not queens turn for draft or fresh" { + assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false; +} + +test "not queens turn for stacked on open" { + assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false; +} + +// ---- the two scenarios of #9032, end to end ---- + +test "scenario bee-reviewed but still open finishes the merge" { + assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE; +} + +test "scenario failed read is never reported as free" { + assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN; +} diff --git a/apps/website/public/widgets/hive-jam/tool.js b/apps/website/public/widgets/hive-jam/tool.js new file mode 100644 index 0000000000..1b0bfea2bf --- /dev/null +++ b/apps/website/public/widgets/hive-jam/tool.js @@ -0,0 +1,36 @@ +// hive-jam/tool.js -- lesson 13 of queen-and-hive: which jam a stuck pull request is in. The lit +// rule, where the card sits, is jam_case of the nine facts, the fn body of gHashTag/t27 +// specs/automation/review-jam.t27 compiled to wasm (./logic.js). The order of the rungs is read from +// jam_case too, not written down: see ORDER below. Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-jam.t27. +import { jamCase } from './logic.js' +import { K, W, ask, controls, el, head, ladder, root, say, source, taps } from '../hive-kit.js' + +const NF = 9, NC = K('K_CASES', 10) +const facts = (f) => [...Array(NF)].map((_, i) => !!(f & (1 << i))) + +// The rule order, asked of jam_case: CLEAR is the first set of facts it calls JAM_NONE; a case's +// trigger is the fewest facts flipped from CLEAR that land on it; of two cases, the earlier rule is +// the one jam_case names with both triggers flipped. A rule's place is how many cases it beats. +const ALL = [...Array(1 << NF).keys()], CASE_OF = ALL.map((f) => jamCase(...facts(f))) +const CLEAR = ALL.find((f) => CASE_OF[f] === K('K_JAM_NONE', 0)) +const flips = (f) => (f ^ CLEAR).toString(2).split('1').length - 1 +const TRIG = [...Array(NC)].map((_, k) => ALL.filter((f) => CASE_OF[f] === k).reduce((a, f) => (flips(f) < flips(a) ? f : a)) ^ CLEAR) +const WINS = TRIG.map((ta, a) => TRIG.filter((tb, b) => b !== a && CASE_OF[CLEAR ^ (ta | tb)] === a).length) +const ORDER = [...TRIG.keys()].sort((a, b) => WINS[b] - WINS[a]) + +const ctl = controls([{ key: 'f', kind: 'hidden', min: 0, max: 511 }], { names: W.SAY_PRESET_NAMES, values: { f: K('K_PRESET_FLAGS', []) } }, draw) +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const CASE_TONES = [0, 2, 8, 6, 1, 5, 4, 3, 8, 6] + +function draw(s) { + const on = facts(s.f) + const c = jamCase(...on) + out.replaceChildren( + taps(on.map((v, i) => ({ label: say('SAY_FLAGS', i), pressed: v, tone: 2 })), (i) => ctl.set('f', s.f ^ (1 << i)), say('SAY_ASK')), + head(say('SAY_RULES')), + ladder(ORDER.map((k, n) => ({ label: `${n + 1}. ${say('SAY_CASE_NAMES', k)}`, sub: k === c ? `${say('SAY_PR')} \u00b7 ${say('SAY_CASE')}` : null, tone: CASE_TONES[k], on: k === c })))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-key-error/card.png b/apps/website/public/widgets/hive-key-error/card.png new file mode 100644 index 0000000000..3e9262de23 Binary files /dev/null and b/apps/website/public/widgets/hive-key-error/card.png differ diff --git a/apps/website/public/widgets/hive-key-error/hive-key-error.t27 b/apps/website/public/widgets/hive-key-error/hive-key-error.t27 new file mode 100644 index 0000000000..b8dccae6d5 --- /dev/null +++ b/apps/website/public/widgets/hive-key-error/hive-key-error.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-key-error.t27 -- A refused key: transient, quota, auth or a bad request +; Source of truth for public/widgets/hive-key-error/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-key-error/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 16 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a status and a code; the verdict is error_kind(status, code), with +; key_closes and charges_issue of its kind. Every grid cell is error_kind of its row and column. + +; WHAT IT DOES NOT CLAIM: it calls no provider: the statuses and codes are a list to pick from. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_key_error; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-key-error"; +pub const TITLE : str = "A refused key: transient, quota, auth or a bad request"; +pub const DESCRIPTION : str = "Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way."; +pub const IMAGE_ALT : str = "Key error card: HTTP 429 with z.ai code 1305. The verdict reads TRANSIENT in gold: the key rests, it does not close, the issue is not charged. A grid of 11 statuses by 7 codes is coloured gold for transient, orange for quota, pink for auth, red for request and green for ok."; +pub const HOOK : str = "z.ai sends a spent quota and a passing overload with the same 429. So the code in the body wins over the status, and 1305 never closes a key."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is error_kind, key_closes and charges_issue, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 16 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STATUSES : [11]u16 = [0, 200, 400, 401, 402, 403, 404, 408, 429, 500, 503]; +pub const K_CODES : [7]u16 = [0, 1113, 1302, 1305, 1308, 1312, 1321]; +pub const K_ZAI_OVERLOAD : u32 = 1305; +pub const K_ZAI_RATE_LIMIT : u32 = 1302; +pub const K_PRESET_STATUS : [4]u8 = [8, 8, 3, 0]; +pub const K_PRESET_CODE : [4]u8 = [3, 1, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["429, code 1305", "429, code 1113", "401", "No answer at all"]; +pub const SAY_ASK : str = "A bee's key was refused. Should it wait, switch keys, or give up?"; +pub const SAY_STATUS : str = "HTTP status (0: no answer)"; +pub const SAY_CODE : str = "z.ai code in the body (0: none)"; +pub const SAY_KIND_NAMES : [5]str = ["OK", "TRANSIENT", "QUOTA", "AUTH", "REQUEST"]; +pub const SAY_KIND_LINES : [5]str = ["answered", "another key or a moment later may pass", "the key's quota, plan or balance is spent", "the key itself is refused", "the request is wrong; every key would say the same"]; +pub const SAY_CLOSES : str = "closes the key"; +pub const SAY_CHARGES : str = "charged to the issue"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_GRID : str = "status by code: the kind error_kind gives"; +pub const SAY_NOTE : str = "Only a wrong request is charged to the issue: an overloaded, spent or refusing provider says nothing about the work, so it never retires an issue."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_lists_hold_the_overload_codes { + assert K_CODES[2] == K_ZAI_RATE_LIMIT; + assert K_CODES[3] == K_ZAI_OVERLOAD; + assert K_STATUSES[8] == 429; +} + +test the_presets_index_the_lists { + assert K_PRESET_STATUS[0] == 8; + assert K_PRESET_CODE[0] == 3; + assert K_STATUSES[0] == 0; +} diff --git a/apps/website/public/widgets/hive-key-error/index.html b/apps/website/public/widgets/hive-key-error/index.html new file mode 100644 index 0000000000..4e997ec58b --- /dev/null +++ b/apps/website/public/widgets/hive-key-error/index.html @@ -0,0 +1,47 @@ + + + + + + +A refused key: transient, quota, auth or a bad request · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

A refused key: transient, quota, auth or a bad request

+

Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is error_kind, key_closes and charges_issue, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 16 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-key-error/logic.js b/apps/website/public/widgets/hive-key-error/logic.js new file mode 100644 index 0000000000..04828ff4e8 --- /dev/null +++ b/apps/website/public/widgets/hive-key-error/logic.js @@ -0,0 +1,60 @@ +// Generated by scripts/t27-logic.mjs from key_failover.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 822367357fc6b487c1c7fb7ac8369165e6a7e6636fd9b0dcafec29e9a6274301 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-key-error/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHgRgAX8Bf2AGf39/f39/AX9gAn9/AX9gBH9/f38BfwMLCgABAgMCAAACAgAEBQFwAQEBBQMBABAGCQF/AUGAgMAACwezAQsGbWVtb3J5AgARdDI3X2NoYXJnZXNfaXNzdWUAAAh0Mjdfc3RlcAABE3QyN19wb29sX292ZXJsb2FkZWQAAg10MjdfbmV4dF9mcmVlAAMKdDI3X2lzX3NldAAEB3QyN19iaXQABQ50Mjdfa2V5X2Nsb3NlcwAGFHQyN19jb29sZG93bl9zZWNvbmRzAAcOdDI3X2Vycm9yX2tpbmQACBF0MjdfaXNfcXVvdGFfY29kZQAJCqsECgcAIABBBEYLagEBfwJAIAANAEEADwtBAyEGAkAgAEEDTQ0AQQQPCwJAIAFBB0sNAEEBIQZBASEBAkAgAEEBRw0AIANBA0kgBEF/aiADT3EhAQsCQCABRQ0AIAJB/wFHDQELQQNBAiAFQf8BRhshBgsgBgsQACAAQQJLIAFBf2ogAElyC2IBAX8CQCACQSBLDQAgA0EBakEAIAMgAkkiAxshBCABIAByIQAgAiADayEDA0AgA0UNASAEIAJwIQEgA0F/aiEDIARBAWohBEEAQQEgAXQgAUEfSxsgAHENAAsgAQ8LQf8BCxUAQQBBASABdCABQR9LGyAAcUEARwsPAEEAQQEgAHQgAEEfSxsLCwAgAEH+AXFBAkYLUwEBf0GIDiECAkACQAJAIABBf2oOAwACAgELQQ8hAkEAIQADQCABIABGDQICQCAAQQRHDQBB8AEPCyAAQQFqIQAgAkEBdCECDAALC0EAIQILIAILkwEBAX9BAiECAkAgAUHZCEYNAAJAIAFBoApGDQAgAUHkdWpBDkkNAUEBIQIgAUHqdWoOBAEAAAEAC0EBIQICQAJAAkACQAJAIABB73xqDggDAgMBAQEBBQALIABFDQQgAEGtA0YNBAsgAEHXBE0NAkEEDwtBAg8LQQMPCyAAQfMDSw0AQQRBACAAQY8DSxshAgsgAgsnAAJAIABB2QhHDQBBAQ8LAkAgAEGgCkcNAEEADwsgAEHkdWpBDkkL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function isQuotaCode(code) { + return (W.t27_is_quota_code(int(code, 0, 4294967295, "is_quota_code.code")) & 1) !== 0; +} +function errorKind(status, zaiCode) { + return W.t27_error_kind(int(status, 0, 4294967295, "error_kind.status"), int(zaiCode, 0, 4294967295, "error_kind.zai_code")) & 255; +} +function cooldownSeconds(kind, strikes) { + return W.t27_cooldown_seconds(int(kind, 0, 255, "cooldown_seconds.kind"), int(strikes, 0, 4294967295, "cooldown_seconds.strikes")) >>> 0; +} +function keyCloses(kind) { + return (W.t27_key_closes(int(kind, 0, 255, "key_closes.kind")) & 1) !== 0; +} +function bit(i) { + return W.t27_bit(int(i, 0, 4294967295, "bit.i")) >>> 0; +} +function isSet(mask, i) { + return (W.t27_is_set(int(mask, 0, 4294967295, "is_set.mask"), int(i, 0, 4294967295, "is_set.i")) & 1) !== 0; +} +function nextFree(tried, blocked, size, last) { + return W.t27_next_free(int(tried, 0, 4294967295, "next_free.tried"), int(blocked, 0, 4294967295, "next_free.blocked"), int(size, 0, 4294967295, "next_free.size"), int(last, 0, 4294967295, "next_free.last")) >>> 0; +} +function poolOverloaded(distinctKeys1305, poolSize) { + return (W.t27_pool_overloaded(int(distinctKeys1305, 0, 4294967295, "pool_overloaded.distinct_keys_1305"), int(poolSize, 0, 4294967295, "pool_overloaded.pool_size")) & 1) !== 0; +} +function step(kind, attempts, nextKey, distinctKeys1305, poolSize, nextPool) { + return W.t27_step(int(kind, 0, 255, "step.kind"), int(attempts, 0, 4294967295, "step.attempts"), int(nextKey, 0, 4294967295, "step.next_key"), int(distinctKeys1305, 0, 4294967295, "step.distinct_keys_1305"), int(poolSize, 0, 4294967295, "step.pool_size"), int(nextPool, 0, 4294967295, "step.next_pool")) & 255; +} +function chargesIssue(kind) { + return (W.t27_charges_issue(int(kind, 0, 255, "charges_issue.kind")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + bit, + chargesIssue, + cooldownSeconds, + errorKind, + isQuotaCode, + isSet, + keyCloses, + nextFree, + poolOverloaded, + step +}; diff --git a/apps/website/public/widgets/hive-key-error/logic.t27 b/apps/website/public/widgets/hive-key-error/logic.t27 new file mode 100644 index 0000000000..3ce96bef2b --- /dev/null +++ b/apps/website/public/widgets/hive-key-error/logic.t27 @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993). +; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them +; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts +; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48 +; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54. +; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider, +; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no +; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and +; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal +; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not. +; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM +; router's cooldown-then-fallback: +; 1. classify the refusal: transient, quota, auth or a bad request (error_kind); +; 2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes +; it for the long break -- 1302 and 1305 never close a key and never retire work; +; 3. the next attempt goes to the next free key of the pool, never the key that just failed +; (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS +; distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next +; pool instead of burning the rest (pool_overloaded); +; 4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708), +; its claim is released (trios#1886), and the end is not charged to the issue (charges_issue). +; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most +; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides. +; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenKeyFailover; + +; --- 1. What the provider said ----------------------------------------------------------------------- + +/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may +/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused. +/// KE_REQUEST: the request is wrong; every key would answer the same. +pub const KE_OK : u8 = 0; +pub const KE_TRANSIENT : u8 = 1; +pub const KE_QUOTA : u8 = 2; +pub const KE_AUTH : u8 = 3; +pub const KE_REQUEST : u8 = 4; + +/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own. +pub const ZAI_RATE_LIMIT : u32 = 1302; +pub const ZAI_OVERLOAD : u32 = 1305; + +/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321 +/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301). +pub fn is_quota_code(code: u32) -> bool { + if (code == 1113) { return true; } + if (code == 1312) { return false; } + if (code < 1308) { return false; } + return code <= 1321; +} + +/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is +/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai +/// sends a spent quota and a passing overload with the same 429. +pub fn error_kind(status: u32, zai_code: u32) -> u8 { + if (is_quota_code(zai_code)) { return KE_QUOTA; } + if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; } + if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; } + if (status == 0) { return KE_TRANSIENT; } + if (status == 402) { return KE_QUOTA; } + if (status == 401) { return KE_AUTH; } + if (status == 403) { return KE_AUTH; } + if (status == 408) { return KE_TRANSIENT; } + if (status == 429) { return KE_TRANSIENT; } + if (status >= 600) { return KE_REQUEST; } + if (status >= 500) { return KE_TRANSIENT; } + if (status >= 400) { return KE_REQUEST; } + return KE_OK; +} + +; --- 2. How long a refused key rests ------------------------------------------------------------------ + +/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than +/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short +/// enough that 34 keys are never all resting through a one-minute burst like 14:08. +pub const COOL_BASE_SECONDS : u32 = 15; +pub const COOL_CAP_SECONDS : u32 = 240; +/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes). +pub const KEY_BREAK_SECONDS : u32 = 1800; + +/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled +/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it. +pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 { + if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; } + if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; } + if (kind != KE_TRANSIENT) { return 0; } + var wait : u32 = COOL_BASE_SECONDS; + var n : u32 = 0; + while (n < strikes) { + if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + return wait; +} + +/// Whether the refusal closes the key for the long break. A transient refusal never does. +pub fn key_closes(kind: u8) -> bool { + if (kind == KE_QUOTA) { return true; } + return kind == KE_AUTH; +} + +; --- 3. The next key, the next pool ------------------------------------------------------------------ + +pub const POOL_MAX : u32 = 32; +pub const NO_KEY : u32 = 255; + +pub fn bit(i: u32) -> u32 { + if (i >= POOL_MAX) { return 0; } + return 1 << i; +} + +pub fn is_set(mask: u32, i: u32) -> bool { + return (mask & bit(i)) != 0; +} + +/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or +/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of +/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than +/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule +/// picks a key inside a pool and a pool inside the pool list. +pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 { + if (size > POOL_MAX) { return NO_KEY; } + var start : u32 = 0; + var count : u32 = size; + if (last < size) { + start = last + 1; + count = size - 1; + } + var k : u32 = 0; + while (k < count) { + var i : u32 = (start + k) % size; + if (is_set(tried | blocked, i) == false) { return i; } + k = k + 1; + } + return NO_KEY; +} + +/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the +/// pool's model overloaded. A pool smaller than that is overloaded when every key said it. +pub const POOL_OVERLOAD_KEYS : u32 = 3; +pub const OVERLOAD_WINDOW_SECONDS : u32 = 60; + +pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool { + if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; } + if (pool_size == 0) { return false; } + return distinct_keys_1305 >= pool_size; +} + +; --- 4. The step after a refusal --------------------------------------------------------------------- + +/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next +/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong. +pub const FA_DONE : u8 = 0; +pub const FA_NEXT_KEY : u8 = 1; +pub const FA_NEXT_POOL : u8 = 2; +pub const FA_GIVE_UP : u8 = 3; +pub const FA_FAIL : u8 = 4; + +/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an +/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys. +pub const MAX_ATTEMPTS_PER_TURN : u32 = 8; + +/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool` +/// are next_free's answers for the current pool and for the pool list (NO_KEY when none). +pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 { + if (kind == KE_OK) { return FA_DONE; } + if (kind >= KE_REQUEST) { return FA_FAIL; } + if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; } + var pool_out : bool = false; + if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); } + if (pool_out == false) { + if (next_key != NO_KEY) { return FA_NEXT_KEY; } + } + if (next_pool != NO_KEY) { return FA_NEXT_POOL; } + return FA_GIVE_UP; +} + +/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded, +/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request +/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges +/// every DX_PROVIDER_ERROR. +pub fn charges_issue(kind: u8) -> bool { + return kind == KE_REQUEST; +} + +; --- 5. Tests ---------------------------------------------------------------------------------------- + +test the_quota_list_is_the_closed_list_of_the_runtime { + assert is_quota_code(1113) == true; + assert is_quota_code(1308) == true; + assert is_quota_code(1311) == true; + assert is_quota_code(1312) == false; + assert is_quota_code(1313) == true; + assert is_quota_code(1321) == true; + assert is_quota_code(1322) == false; + assert is_quota_code(1307) == false; + assert is_quota_code(1112) == false; + assert is_quota_code(1114) == false; + assert is_quota_code(ZAI_RATE_LIMIT) == false; + assert is_quota_code(ZAI_OVERLOAD) == false; + assert is_quota_code(0) == false; +} + +test the_code_wins_over_the_status { + assert error_kind(429, 1305) == KE_TRANSIENT; + assert error_kind(429, 1302) == KE_TRANSIENT; + assert error_kind(429, 1113) == KE_QUOTA; + assert error_kind(429, 1316) == KE_QUOTA; + assert error_kind(200, 1308) == KE_QUOTA; + assert error_kind(400, 1305) == KE_TRANSIENT; + assert error_kind(400, 1302) == KE_TRANSIENT; + assert error_kind(429, 1312) == KE_TRANSIENT; +} + +test a_status_without_a_code_falls_in_its_kind { + assert error_kind(0, 0) == KE_TRANSIENT; + assert error_kind(200, 0) == KE_OK; + assert error_kind(399, 0) == KE_OK; + assert error_kind(400, 0) == KE_REQUEST; + assert error_kind(401, 0) == KE_AUTH; + assert error_kind(402, 0) == KE_QUOTA; + assert error_kind(403, 0) == KE_AUTH; + assert error_kind(404, 0) == KE_REQUEST; + assert error_kind(407, 0) == KE_REQUEST; + assert error_kind(408, 0) == KE_TRANSIENT; + assert error_kind(409, 0) == KE_REQUEST; + assert error_kind(428, 0) == KE_REQUEST; + assert error_kind(429, 0) == KE_TRANSIENT; + assert error_kind(430, 0) == KE_REQUEST; + assert error_kind(499, 0) == KE_REQUEST; + assert error_kind(500, 0) == KE_TRANSIENT; + assert error_kind(503, 0) == KE_TRANSIENT; + assert error_kind(529, 0) == KE_TRANSIENT; + assert error_kind(599, 0) == KE_TRANSIENT; + assert error_kind(600, 0) == KE_REQUEST; +} + +test a_transient_refusal_rests_seconds_and_a_spent_key_closes { + assert cooldown_seconds(KE_TRANSIENT, 0) == 15; + assert cooldown_seconds(KE_TRANSIENT, 1) == 30; + assert cooldown_seconds(KE_TRANSIENT, 2) == 60; + assert cooldown_seconds(KE_TRANSIENT, 3) == 120; + assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_OK, 3) == 0; + assert cooldown_seconds(KE_REQUEST, 3) == 0; + assert key_closes(KE_QUOTA) == true; + assert key_closes(KE_AUTH) == true; + assert key_closes(KE_TRANSIENT) == false; + assert key_closes(KE_OK) == false; + assert key_closes(KE_REQUEST) == false; +} + +test bits_name_keys { + assert bit(0) == 1; + assert bit(3) == 8; + assert bit(31) == 2147483648; + assert bit(32) == 0; + assert is_set(8, 3) == true; + assert is_set(8, 2) == false; + assert is_set(4294967295, 31) == true; + assert is_set(4294967295, 32) == false; +} + +test the_next_key_is_never_the_one_that_failed { + // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0 + assert next_free(bit(9), 0, 10, 9) == 0; + // even when the failed key is not marked tried, it is skipped + assert next_free(0, 0, 10, 9) == 0; + assert next_free(0, 0, 10, 4) == 5; + // a pool of one key has no other key + assert next_free(0, 0, 1, 0) == NO_KEY; + // tried and resting keys are both skipped + assert next_free(bit(5), bit(6), 10, 4) == 7; + assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY; + // a fresh turn starts at key 0 + assert next_free(0, 0, 24, NO_KEY) == 0; + assert next_free(bit(0), 0, 24, NO_KEY) == 1; + // every key but the failed one is taken + assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY; + // an empty pool has nothing + assert next_free(0, 0, 0, NO_KEY) == NO_KEY; + // `last` just past the end is out of range: the scan starts at 0 + assert next_free(0, 0, 10, 10) == 0; + // a full mask of POOL_MAX keys wraps from the top index + assert next_free(0, 0, 32, 31) == 0; + assert next_free(0, 0, 32, NO_KEY) == 0; + assert next_free(0, 4294967295 - bit(31), 32, 30) == 31; + // a list longer than POOL_MAX does not fit a mask and is refused + assert next_free(0, 0, 33, NO_KEY) == NO_KEY; + assert next_free(0, 0, 40, 31) == NO_KEY; +} + +test an_overload_seen_on_three_keys_is_the_pools_model { + assert pool_overloaded(0, 24) == false; + assert pool_overloaded(2, 24) == false; + assert pool_overloaded(3, 24) == true; + assert pool_overloaded(16, 24) == true; + assert pool_overloaded(2, 2) == true; + assert pool_overloaded(1, 2) == false; + assert pool_overloaded(1, 1) == true; + assert pool_overloaded(0, 0) == false; +} + +/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that +/// answered 1305 in the window, pool size, next pool. +test conformance_rows { + // r01 answered: go on + assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE; + // r02 a wrong request fails, whatever keys are left + assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL; + // r03 an unknown kind is read as a wrong request + assert step(9, 1, 5, 0, 24, 1) == FA_FAIL; + // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key + assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY; + // r05 a rate limit (1302) on a fresh pool: next key + assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY; + // r06 the third distinct key said 1305: the model is overloaded, next pool + assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL; + // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest + assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP; + // r08 a spent quota on one key does not mean an overloaded pool: next key + assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY; + // r09 a refused key: next key + assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY; + // r10 no key left in the pool: next pool + assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL; + // r11 no key and no pool left: give up + assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP; + // r12 the attempt bound stops a turn with keys still left + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP; + // r13 one attempt below the bound still moves on + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY; + // r14 a two-key pool where both said 1305 is overloaded + assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL; + // r15 a 503 with no 1305 seen keeps to the pool + assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY; + // r16 the bound beats a wrong request? no: a wrong request fails first + assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL; + // r17 an answer after many attempts is still an answer + assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE; +} + +test an_overloaded_provider_never_retires_the_issue { + assert charges_issue(KE_TRANSIENT) == false; + assert charges_issue(KE_QUOTA) == false; + assert charges_issue(KE_AUTH) == false; + assert charges_issue(KE_REQUEST) == true; + assert charges_issue(KE_OK) == false; + assert charges_issue(9) == false; +} + +; --- 6. Invariants ----------------------------------------------------------------------------------- + +invariant the_kinds_and_steps_are_distinct { + assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4; + assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4; +} + +/// A transient rest is always shorter than the long break, so a 1305 can never close a key. +invariant a_rest_is_shorter_than_a_break { + assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16; + assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800; +} + +/// The bound leaves room to see an overload in one pool and still try the next: the overload is +/// detected before the bound stops the turn. +invariant the_bound_leaves_room_for_a_second_pool { + assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2; + assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0; +} + +/// NO_KEY is no index a mask can hold. +invariant no_key_is_out_of_range { + assert POOL_MAX == 32 and NO_KEY >= POOL_MAX; + assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305; +} diff --git a/apps/website/public/widgets/hive-key-error/tool.js b/apps/website/public/widgets/hive-key-error/tool.js new file mode 100644 index 0000000000..fb661f41c9 --- /dev/null +++ b/apps/website/public/widgets/hive-key-error/tool.js @@ -0,0 +1,32 @@ +// hive-key-error/tool.js -- lesson 16 of queen-and-hive: what kind of refusal a bee's key got. The +// verdict is error_kind(status, code) with key_closes and charges_issue of its kind, and every grid +// cell is error_kind of its row and column: fn bodies of gHashTag/t27 specs/queen/key_failover.t27 +// compiled to wasm (./logic.js). Words are SAY_ and numbers K_ constants of +// specs/widgets/hive-key-error.t27. +import { chargesIssue, errorKind, keyCloses } from './logic.js' +import { K, W, ask, controls, el, grid, head, root, say, source, tiles, verdict } from '../hive-kit.js' + +const ST = K('K_STATUSES', []), CO = K('K_CODES', []) +const ctl = controls([ + { key: 'st', label: say('SAY_STATUS'), kind: 'choice', options: ST.map(String) }, + { key: 'co', label: say('SAY_CODE'), kind: 'choice', options: CO.map(String) }, +], { names: W.SAY_PRESET_NAMES, values: { st: K('K_PRESET_STATUS', []), co: K('K_PRESET_CODE', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const KIND_TONES = [0, 1, 4, 3, 6] + +function draw(s) { + const kind = errorKind(ST[s.st], CO[s.co]) + const closes = keyCloses(kind), charges = chargesIssue(kind) + out.replaceChildren( + verdict(say('SAY_KIND_NAMES', kind), KIND_TONES[kind], say('SAY_KIND_LINES', kind)), + tiles([ + { label: say('SAY_CLOSES'), value: closes ? say('SAY_YES') : say('SAY_NO'), tone: closes ? 6 : 0 }, + { label: say('SAY_CHARGES'), value: charges ? say('SAY_YES') : say('SAY_NO'), tone: charges ? 6 : 0 }, + ]), + head(say('SAY_GRID')), + grid({ rows: ST.length, cols: CO.length, rowLabels: ST.map(String), colLabels: CO.map(String), cell: (r, c) => { const k = errorKind(ST[r], CO[c]); return { text: say('SAY_KIND_NAMES', k).slice(0, 1), tone: KIND_TONES[k], on: r === s.st && c === s.co ? true : k !== 0 } } })) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-key-rest/card.png b/apps/website/public/widgets/hive-key-rest/card.png new file mode 100644 index 0000000000..aebfd488f5 Binary files /dev/null and b/apps/website/public/widgets/hive-key-rest/card.png differ diff --git a/apps/website/public/widgets/hive-key-rest/hive-key-rest.t27 b/apps/website/public/widgets/hive-key-rest/hive-key-rest.t27 new file mode 100644 index 0000000000..c8c768377a --- /dev/null +++ b/apps/website/public/widgets/hive-key-rest/hive-key-rest.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-key-rest.t27 -- How long a refused key rests +; Source of truth for public/widgets/hive-key-rest/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-key-rest/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 17 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a kind and the strikes in a row. Every rung is +; cooldown_seconds(transient, strikes); the dial and the verdict are cooldown_seconds of the picked +; kind. + +; WHAT IT DOES NOT CLAIM: it times nothing: the strikes are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_key_rest; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-key-rest"; +pub const TITLE : str = "How long a refused key rests"; +pub const DESCRIPTION : str = "A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes."; +pub const IMAGE_ALT : str = "Key rest card: a transient refusal after 3 strikes. Rungs for 0 to 6 strikes read 15, 30, 60, 120, 240, 240, 240 seconds, the fourth lit. The dial shows 120 s against the 1800 s long break."; +pub const HOOK : str = "Long enough that the next bee does not hit the same overloaded key at once, short enough that 34 keys are never all resting through a one-minute burst."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is cooldown_seconds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 17 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STRIKES_MAX : u8 = 8; +pub const K_KE_TRANSIENT : u8 = 1; +pub const K_RUNGS : u8 = 7; +pub const K_COOL_BASE_SECONDS : u32 = 15; +pub const K_COOL_CAP_SECONDS : u32 = 240; +pub const K_KEY_BREAK_SECONDS : u32 = 1800; +pub const K_PRESET_KIND : [4]u8 = [1, 1, 2, 4]; +pub const K_PRESET_STRIKES : [4]u8 = [3, 6, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Transient, 3 strikes", "Transient, 6 strikes", "Quota spent", "A bad request"]; +pub const SAY_ASK : str = "How long should a key rest after it was refused?"; +pub const SAY_KIND : str = "kind of refusal"; +pub const SAY_KIND_NAMES : [5]str = ["ok", "transient", "quota", "auth", "request"]; +pub const SAY_STRIKES : str = "strikes in a row before this one"; +pub const SAY_RUNG : str = "after {0} in a row"; +pub const SAY_REST : str = "rests {0} s"; +pub const SAY_DIAL : str = "of the {0} s break"; +pub const SAY_NONE : str = "no rest"; +pub const SAY_NOTE : str = "The cap is the base doubled exactly four times, so the doubling meets 240 and never steps past it. A rest is always shorter than the break, so a 1305 can never close a key."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_rest_and_the_break { + assert K_COOL_BASE_SECONDS == 15; + assert K_COOL_CAP_SECONDS == 240; + assert K_KEY_BREAK_SECONDS == 1800; +} + +test the_rungs_fit_the_slider { + assert K_RUNGS <= K_STRIKES_MAX; + assert K_PRESET_STRIKES[1] < K_RUNGS; +} diff --git a/apps/website/public/widgets/hive-key-rest/index.html b/apps/website/public/widgets/hive-key-rest/index.html new file mode 100644 index 0000000000..106da0bceb --- /dev/null +++ b/apps/website/public/widgets/hive-key-rest/index.html @@ -0,0 +1,47 @@ + + + + + + +How long a refused key rests · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

How long a refused key rests

+

A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is cooldown_seconds, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 17 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-key-rest/logic.js b/apps/website/public/widgets/hive-key-rest/logic.js new file mode 100644 index 0000000000..e14b9a1b5f --- /dev/null +++ b/apps/website/public/widgets/hive-key-rest/logic.js @@ -0,0 +1,60 @@ +// Generated by scripts/t27-logic.mjs from key_failover.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 822367357fc6b487c1c7fb7ac8369165e6a7e6636fd9b0dcafec29e9a6274301 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-key-rest/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHgRgAX8Bf2AGf39/f39/AX9gAn9/AX9gBH9/f38BfwMLCgABAgMCAAACAgAEBQFwAQEBBQMBABAGCQF/AUGAgMAACwezAQsGbWVtb3J5AgARdDI3X2NoYXJnZXNfaXNzdWUAAAh0Mjdfc3RlcAABE3QyN19wb29sX292ZXJsb2FkZWQAAg10MjdfbmV4dF9mcmVlAAMKdDI3X2lzX3NldAAEB3QyN19iaXQABQ50Mjdfa2V5X2Nsb3NlcwAGFHQyN19jb29sZG93bl9zZWNvbmRzAAcOdDI3X2Vycm9yX2tpbmQACBF0MjdfaXNfcXVvdGFfY29kZQAJCqsECgcAIABBBEYLagEBfwJAIAANAEEADwtBAyEGAkAgAEEDTQ0AQQQPCwJAIAFBB0sNAEEBIQZBASEBAkAgAEEBRw0AIANBA0kgBEF/aiADT3EhAQsCQCABRQ0AIAJB/wFHDQELQQNBAiAFQf8BRhshBgsgBgsQACAAQQJLIAFBf2ogAElyC2IBAX8CQCACQSBLDQAgA0EBakEAIAMgAkkiAxshBCABIAByIQAgAiADayEDA0AgA0UNASAEIAJwIQEgA0F/aiEDIARBAWohBEEAQQEgAXQgAUEfSxsgAHENAAsgAQ8LQf8BCxUAQQBBASABdCABQR9LGyAAcUEARwsPAEEAQQEgAHQgAEEfSxsLCwAgAEH+AXFBAkYLUwEBf0GIDiECAkACQAJAIABBf2oOAwACAgELQQ8hAkEAIQADQCABIABGDQICQCAAQQRHDQBB8AEPCyAAQQFqIQAgAkEBdCECDAALC0EAIQILIAILkwEBAX9BAiECAkAgAUHZCEYNAAJAIAFBoApGDQAgAUHkdWpBDkkNAUEBIQIgAUHqdWoOBAEAAAEAC0EBIQICQAJAAkACQAJAIABB73xqDggDAgMBAQEBBQALIABFDQQgAEGtA0YNBAsgAEHXBE0NAkEEDwtBAg8LQQMPCyAAQfMDSw0AQQRBACAAQY8DSxshAgsgAgsnAAJAIABB2QhHDQBBAQ8LAkAgAEGgCkcNAEEADwsgAEHkdWpBDkkL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function isQuotaCode(code) { + return (W.t27_is_quota_code(int(code, 0, 4294967295, "is_quota_code.code")) & 1) !== 0; +} +function errorKind(status, zaiCode) { + return W.t27_error_kind(int(status, 0, 4294967295, "error_kind.status"), int(zaiCode, 0, 4294967295, "error_kind.zai_code")) & 255; +} +function cooldownSeconds(kind, strikes) { + return W.t27_cooldown_seconds(int(kind, 0, 255, "cooldown_seconds.kind"), int(strikes, 0, 4294967295, "cooldown_seconds.strikes")) >>> 0; +} +function keyCloses(kind) { + return (W.t27_key_closes(int(kind, 0, 255, "key_closes.kind")) & 1) !== 0; +} +function bit(i) { + return W.t27_bit(int(i, 0, 4294967295, "bit.i")) >>> 0; +} +function isSet(mask, i) { + return (W.t27_is_set(int(mask, 0, 4294967295, "is_set.mask"), int(i, 0, 4294967295, "is_set.i")) & 1) !== 0; +} +function nextFree(tried, blocked, size, last) { + return W.t27_next_free(int(tried, 0, 4294967295, "next_free.tried"), int(blocked, 0, 4294967295, "next_free.blocked"), int(size, 0, 4294967295, "next_free.size"), int(last, 0, 4294967295, "next_free.last")) >>> 0; +} +function poolOverloaded(distinctKeys1305, poolSize) { + return (W.t27_pool_overloaded(int(distinctKeys1305, 0, 4294967295, "pool_overloaded.distinct_keys_1305"), int(poolSize, 0, 4294967295, "pool_overloaded.pool_size")) & 1) !== 0; +} +function step(kind, attempts, nextKey, distinctKeys1305, poolSize, nextPool) { + return W.t27_step(int(kind, 0, 255, "step.kind"), int(attempts, 0, 4294967295, "step.attempts"), int(nextKey, 0, 4294967295, "step.next_key"), int(distinctKeys1305, 0, 4294967295, "step.distinct_keys_1305"), int(poolSize, 0, 4294967295, "step.pool_size"), int(nextPool, 0, 4294967295, "step.next_pool")) & 255; +} +function chargesIssue(kind) { + return (W.t27_charges_issue(int(kind, 0, 255, "charges_issue.kind")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + bit, + chargesIssue, + cooldownSeconds, + errorKind, + isQuotaCode, + isSet, + keyCloses, + nextFree, + poolOverloaded, + step +}; diff --git a/apps/website/public/widgets/hive-key-rest/logic.t27 b/apps/website/public/widgets/hive-key-rest/logic.t27 new file mode 100644 index 0000000000..3ce96bef2b --- /dev/null +++ b/apps/website/public/widgets/hive-key-rest/logic.t27 @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993). +; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them +; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts +; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48 +; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54. +; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider, +; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no +; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and +; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal +; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not. +; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM +; router's cooldown-then-fallback: +; 1. classify the refusal: transient, quota, auth or a bad request (error_kind); +; 2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes +; it for the long break -- 1302 and 1305 never close a key and never retire work; +; 3. the next attempt goes to the next free key of the pool, never the key that just failed +; (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS +; distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next +; pool instead of burning the rest (pool_overloaded); +; 4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708), +; its claim is released (trios#1886), and the end is not charged to the issue (charges_issue). +; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most +; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides. +; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenKeyFailover; + +; --- 1. What the provider said ----------------------------------------------------------------------- + +/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may +/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused. +/// KE_REQUEST: the request is wrong; every key would answer the same. +pub const KE_OK : u8 = 0; +pub const KE_TRANSIENT : u8 = 1; +pub const KE_QUOTA : u8 = 2; +pub const KE_AUTH : u8 = 3; +pub const KE_REQUEST : u8 = 4; + +/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own. +pub const ZAI_RATE_LIMIT : u32 = 1302; +pub const ZAI_OVERLOAD : u32 = 1305; + +/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321 +/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301). +pub fn is_quota_code(code: u32) -> bool { + if (code == 1113) { return true; } + if (code == 1312) { return false; } + if (code < 1308) { return false; } + return code <= 1321; +} + +/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is +/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai +/// sends a spent quota and a passing overload with the same 429. +pub fn error_kind(status: u32, zai_code: u32) -> u8 { + if (is_quota_code(zai_code)) { return KE_QUOTA; } + if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; } + if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; } + if (status == 0) { return KE_TRANSIENT; } + if (status == 402) { return KE_QUOTA; } + if (status == 401) { return KE_AUTH; } + if (status == 403) { return KE_AUTH; } + if (status == 408) { return KE_TRANSIENT; } + if (status == 429) { return KE_TRANSIENT; } + if (status >= 600) { return KE_REQUEST; } + if (status >= 500) { return KE_TRANSIENT; } + if (status >= 400) { return KE_REQUEST; } + return KE_OK; +} + +; --- 2. How long a refused key rests ------------------------------------------------------------------ + +/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than +/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short +/// enough that 34 keys are never all resting through a one-minute burst like 14:08. +pub const COOL_BASE_SECONDS : u32 = 15; +pub const COOL_CAP_SECONDS : u32 = 240; +/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes). +pub const KEY_BREAK_SECONDS : u32 = 1800; + +/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled +/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it. +pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 { + if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; } + if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; } + if (kind != KE_TRANSIENT) { return 0; } + var wait : u32 = COOL_BASE_SECONDS; + var n : u32 = 0; + while (n < strikes) { + if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + return wait; +} + +/// Whether the refusal closes the key for the long break. A transient refusal never does. +pub fn key_closes(kind: u8) -> bool { + if (kind == KE_QUOTA) { return true; } + return kind == KE_AUTH; +} + +; --- 3. The next key, the next pool ------------------------------------------------------------------ + +pub const POOL_MAX : u32 = 32; +pub const NO_KEY : u32 = 255; + +pub fn bit(i: u32) -> u32 { + if (i >= POOL_MAX) { return 0; } + return 1 << i; +} + +pub fn is_set(mask: u32, i: u32) -> bool { + return (mask & bit(i)) != 0; +} + +/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or +/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of +/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than +/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule +/// picks a key inside a pool and a pool inside the pool list. +pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 { + if (size > POOL_MAX) { return NO_KEY; } + var start : u32 = 0; + var count : u32 = size; + if (last < size) { + start = last + 1; + count = size - 1; + } + var k : u32 = 0; + while (k < count) { + var i : u32 = (start + k) % size; + if (is_set(tried | blocked, i) == false) { return i; } + k = k + 1; + } + return NO_KEY; +} + +/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the +/// pool's model overloaded. A pool smaller than that is overloaded when every key said it. +pub const POOL_OVERLOAD_KEYS : u32 = 3; +pub const OVERLOAD_WINDOW_SECONDS : u32 = 60; + +pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool { + if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; } + if (pool_size == 0) { return false; } + return distinct_keys_1305 >= pool_size; +} + +; --- 4. The step after a refusal --------------------------------------------------------------------- + +/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next +/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong. +pub const FA_DONE : u8 = 0; +pub const FA_NEXT_KEY : u8 = 1; +pub const FA_NEXT_POOL : u8 = 2; +pub const FA_GIVE_UP : u8 = 3; +pub const FA_FAIL : u8 = 4; + +/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an +/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys. +pub const MAX_ATTEMPTS_PER_TURN : u32 = 8; + +/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool` +/// are next_free's answers for the current pool and for the pool list (NO_KEY when none). +pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 { + if (kind == KE_OK) { return FA_DONE; } + if (kind >= KE_REQUEST) { return FA_FAIL; } + if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; } + var pool_out : bool = false; + if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); } + if (pool_out == false) { + if (next_key != NO_KEY) { return FA_NEXT_KEY; } + } + if (next_pool != NO_KEY) { return FA_NEXT_POOL; } + return FA_GIVE_UP; +} + +/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded, +/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request +/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges +/// every DX_PROVIDER_ERROR. +pub fn charges_issue(kind: u8) -> bool { + return kind == KE_REQUEST; +} + +; --- 5. Tests ---------------------------------------------------------------------------------------- + +test the_quota_list_is_the_closed_list_of_the_runtime { + assert is_quota_code(1113) == true; + assert is_quota_code(1308) == true; + assert is_quota_code(1311) == true; + assert is_quota_code(1312) == false; + assert is_quota_code(1313) == true; + assert is_quota_code(1321) == true; + assert is_quota_code(1322) == false; + assert is_quota_code(1307) == false; + assert is_quota_code(1112) == false; + assert is_quota_code(1114) == false; + assert is_quota_code(ZAI_RATE_LIMIT) == false; + assert is_quota_code(ZAI_OVERLOAD) == false; + assert is_quota_code(0) == false; +} + +test the_code_wins_over_the_status { + assert error_kind(429, 1305) == KE_TRANSIENT; + assert error_kind(429, 1302) == KE_TRANSIENT; + assert error_kind(429, 1113) == KE_QUOTA; + assert error_kind(429, 1316) == KE_QUOTA; + assert error_kind(200, 1308) == KE_QUOTA; + assert error_kind(400, 1305) == KE_TRANSIENT; + assert error_kind(400, 1302) == KE_TRANSIENT; + assert error_kind(429, 1312) == KE_TRANSIENT; +} + +test a_status_without_a_code_falls_in_its_kind { + assert error_kind(0, 0) == KE_TRANSIENT; + assert error_kind(200, 0) == KE_OK; + assert error_kind(399, 0) == KE_OK; + assert error_kind(400, 0) == KE_REQUEST; + assert error_kind(401, 0) == KE_AUTH; + assert error_kind(402, 0) == KE_QUOTA; + assert error_kind(403, 0) == KE_AUTH; + assert error_kind(404, 0) == KE_REQUEST; + assert error_kind(407, 0) == KE_REQUEST; + assert error_kind(408, 0) == KE_TRANSIENT; + assert error_kind(409, 0) == KE_REQUEST; + assert error_kind(428, 0) == KE_REQUEST; + assert error_kind(429, 0) == KE_TRANSIENT; + assert error_kind(430, 0) == KE_REQUEST; + assert error_kind(499, 0) == KE_REQUEST; + assert error_kind(500, 0) == KE_TRANSIENT; + assert error_kind(503, 0) == KE_TRANSIENT; + assert error_kind(529, 0) == KE_TRANSIENT; + assert error_kind(599, 0) == KE_TRANSIENT; + assert error_kind(600, 0) == KE_REQUEST; +} + +test a_transient_refusal_rests_seconds_and_a_spent_key_closes { + assert cooldown_seconds(KE_TRANSIENT, 0) == 15; + assert cooldown_seconds(KE_TRANSIENT, 1) == 30; + assert cooldown_seconds(KE_TRANSIENT, 2) == 60; + assert cooldown_seconds(KE_TRANSIENT, 3) == 120; + assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_OK, 3) == 0; + assert cooldown_seconds(KE_REQUEST, 3) == 0; + assert key_closes(KE_QUOTA) == true; + assert key_closes(KE_AUTH) == true; + assert key_closes(KE_TRANSIENT) == false; + assert key_closes(KE_OK) == false; + assert key_closes(KE_REQUEST) == false; +} + +test bits_name_keys { + assert bit(0) == 1; + assert bit(3) == 8; + assert bit(31) == 2147483648; + assert bit(32) == 0; + assert is_set(8, 3) == true; + assert is_set(8, 2) == false; + assert is_set(4294967295, 31) == true; + assert is_set(4294967295, 32) == false; +} + +test the_next_key_is_never_the_one_that_failed { + // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0 + assert next_free(bit(9), 0, 10, 9) == 0; + // even when the failed key is not marked tried, it is skipped + assert next_free(0, 0, 10, 9) == 0; + assert next_free(0, 0, 10, 4) == 5; + // a pool of one key has no other key + assert next_free(0, 0, 1, 0) == NO_KEY; + // tried and resting keys are both skipped + assert next_free(bit(5), bit(6), 10, 4) == 7; + assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY; + // a fresh turn starts at key 0 + assert next_free(0, 0, 24, NO_KEY) == 0; + assert next_free(bit(0), 0, 24, NO_KEY) == 1; + // every key but the failed one is taken + assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY; + // an empty pool has nothing + assert next_free(0, 0, 0, NO_KEY) == NO_KEY; + // `last` just past the end is out of range: the scan starts at 0 + assert next_free(0, 0, 10, 10) == 0; + // a full mask of POOL_MAX keys wraps from the top index + assert next_free(0, 0, 32, 31) == 0; + assert next_free(0, 0, 32, NO_KEY) == 0; + assert next_free(0, 4294967295 - bit(31), 32, 30) == 31; + // a list longer than POOL_MAX does not fit a mask and is refused + assert next_free(0, 0, 33, NO_KEY) == NO_KEY; + assert next_free(0, 0, 40, 31) == NO_KEY; +} + +test an_overload_seen_on_three_keys_is_the_pools_model { + assert pool_overloaded(0, 24) == false; + assert pool_overloaded(2, 24) == false; + assert pool_overloaded(3, 24) == true; + assert pool_overloaded(16, 24) == true; + assert pool_overloaded(2, 2) == true; + assert pool_overloaded(1, 2) == false; + assert pool_overloaded(1, 1) == true; + assert pool_overloaded(0, 0) == false; +} + +/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that +/// answered 1305 in the window, pool size, next pool. +test conformance_rows { + // r01 answered: go on + assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE; + // r02 a wrong request fails, whatever keys are left + assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL; + // r03 an unknown kind is read as a wrong request + assert step(9, 1, 5, 0, 24, 1) == FA_FAIL; + // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key + assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY; + // r05 a rate limit (1302) on a fresh pool: next key + assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY; + // r06 the third distinct key said 1305: the model is overloaded, next pool + assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL; + // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest + assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP; + // r08 a spent quota on one key does not mean an overloaded pool: next key + assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY; + // r09 a refused key: next key + assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY; + // r10 no key left in the pool: next pool + assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL; + // r11 no key and no pool left: give up + assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP; + // r12 the attempt bound stops a turn with keys still left + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP; + // r13 one attempt below the bound still moves on + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY; + // r14 a two-key pool where both said 1305 is overloaded + assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL; + // r15 a 503 with no 1305 seen keeps to the pool + assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY; + // r16 the bound beats a wrong request? no: a wrong request fails first + assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL; + // r17 an answer after many attempts is still an answer + assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE; +} + +test an_overloaded_provider_never_retires_the_issue { + assert charges_issue(KE_TRANSIENT) == false; + assert charges_issue(KE_QUOTA) == false; + assert charges_issue(KE_AUTH) == false; + assert charges_issue(KE_REQUEST) == true; + assert charges_issue(KE_OK) == false; + assert charges_issue(9) == false; +} + +; --- 6. Invariants ----------------------------------------------------------------------------------- + +invariant the_kinds_and_steps_are_distinct { + assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4; + assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4; +} + +/// A transient rest is always shorter than the long break, so a 1305 can never close a key. +invariant a_rest_is_shorter_than_a_break { + assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16; + assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800; +} + +/// The bound leaves room to see an overload in one pool and still try the next: the overload is +/// detected before the bound stops the turn. +invariant the_bound_leaves_room_for_a_second_pool { + assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2; + assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0; +} + +/// NO_KEY is no index a mask can hold. +invariant no_key_is_out_of_range { + assert POOL_MAX == 32 and NO_KEY >= POOL_MAX; + assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305; +} diff --git a/apps/website/public/widgets/hive-key-rest/tool.js b/apps/website/public/widgets/hive-key-rest/tool.js new file mode 100644 index 0000000000..a6aa58645b --- /dev/null +++ b/apps/website/public/widgets/hive-key-rest/tool.js @@ -0,0 +1,25 @@ +// hive-key-rest/tool.js -- lesson 17 of queen-and-hive: how long a refused key rests. Every rung and the +// dial are cooldown_seconds, the fn body of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm +// (./logic.js). Words are SAY_ and numbers K_ constants of specs/widgets/hive-key-rest.t27. +import { cooldownSeconds } from './logic.js' +import { K, W, ask, controls, dial, el, fill, ladder, root, say, source, two } from '../hive-kit.js' + +const TRANSIENT = K('K_KE_TRANSIENT', 1) +const ctl = controls([ + { key: 'kind', label: say('SAY_KIND'), kind: 'choice', options: W.SAY_KIND_NAMES }, + { key: 'strikes', label: say('SAY_STRIKES'), min: 0, max: K('K_STRIKES_MAX', 8), step: 1 }, +], { names: W.SAY_PRESET_NAMES, values: { kind: K('K_PRESET_KIND', []), strikes: K('K_PRESET_STRIKES', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const rest = cooldownSeconds(s.kind, s.strikes) + const brk = K('K_KEY_BREAK_SECONDS', 1800) + out.replaceChildren(two( + ladder([...Array(K('K_RUNGS', 7))].map((_, n) => ({ label: fill(say('SAY_RUNG'), n), sub: fill(say('SAY_REST'), cooldownSeconds(TRANSIENT, n)), tone: 1, on: s.kind === TRANSIENT && n === s.strikes }))), + el('div', {}, dial({ value: rest, max: brk, tone: rest >= brk ? 6 : rest ? 1 : 0, big: rest ? `${rest} s` : say('SAY_NONE'), small: fill(say('SAY_DIAL'), brk), label: fill(say('SAY_DIAL'), brk), marks: [{ at: K('K_COOL_CAP_SECONDS', 240), tone: 1 }] }), + el('p', { class: 'hk-cap' }, `${say('SAY_KIND_NAMES', s.kind)} · ${fill(say('SAY_RUNG'), s.strikes)}`)))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-kit.css b/apps/website/public/widgets/hive-kit.css new file mode 100644 index 0000000000..6002373b24 --- /dev/null +++ b/apps/website/public/widgets/hive-kit.css @@ -0,0 +1,88 @@ +/* hive-kit.css -- the drawing the queen-and-hive widgets add to dsp-kit.css: the opening question, a + kanban board, a review gate, meters, a ladder, a clock dial, comb cells, a grid of answers and tap + rows. Colours carry meaning: green goes, gold waits, red stops, grey is a column with nothing in it. + Every tap target is at least 44px on a phone, and nothing is wider than the 390px layout. */ +.hk-ask { margin: 0 0 10px; padding: 10px 12px; border-left: 4px solid var(--c1); border-radius: 8px; background: color-mix(in srgb, var(--c1) 8%, var(--w-panel)); color: #fff; font: 700 0.95rem/1.4 var(--w-mono); } +.hk-head { margin-top: 12px; } +.hk-pill { --hc: var(--c0); display: inline-block; padding: 2px 8px; border: 1px solid color-mix(in srgb, var(--hc) 60%, transparent); border-radius: 999px; color: var(--hc); background: color-mix(in srgb, var(--hc) 12%, transparent); font: 700 0.72rem var(--w-mono); } +.hk-board { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin: 8px 0; } +@container (min-width: 560px) { .hk-board { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); } } +.hk-col { --hc: var(--c0); min-width: 0; padding: 6px; border: 1px solid color-mix(in srgb, var(--hc) 30%, transparent); border-top: 4px solid color-mix(in srgb, var(--hc) 55%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--hc) 5%, var(--w-panel)); opacity: 0.6; } +.hk-col-full { opacity: 1; border-top-color: var(--hc); } +.hk-col-head { display: flex; justify-content: space-between; gap: 4px; margin: 0 0 6px; color: var(--w-muted); font: 700 0.66rem var(--w-mono); text-transform: uppercase; letter-spacing: 0.04em; overflow-wrap: anywhere; } +.hk-col-head b { color: var(--hc); } +.hk-card { --hc: var(--c0); display: grid; gap: 2px; margin-top: 4px; padding: 6px 8px; border-radius: 8px; border-left: 4px solid var(--hc); background: #0b1210; box-shadow: 0 0 12px color-mix(in srgb, var(--hc) 25%, transparent); animation: hk-land 0.35s ease-out; } +.hk-card strong { color: #fff; font: 700 0.78rem var(--w-mono); overflow-wrap: anywhere; } +.hk-card span { color: var(--w-muted); font: 0.68rem var(--w-mono); overflow-wrap: anywhere; } +.hk-card.hk-on { outline: 2px solid var(--hc); } +.hk-slot { min-height: 34px; border: 1px dashed rgba(173, 201, 192, 0.18); border-radius: 8px; } +@keyframes hk-land { from { transform: translateY(-6px); opacity: 0.2; } to { transform: none; opacity: 1; } } +@media (prefers-reduced-motion: reduce) { .hk-card { animation: none; } } +.hk-gate { display: flex; align-items: center; gap: 12px; margin: 8px 0; padding: 10px; border-radius: 12px; border: 1px solid; } +.hk-gate-art { width: 96px; flex: none; } +.hk-gate strong { display: block; font: 800 1.05rem var(--w-mono); } +.hk-gate span { display: block; color: var(--w-muted); font: 0.78rem/1.4 var(--w-mono); } +.hk-open { border-color: color-mix(in srgb, var(--c0) 60%, transparent); background: color-mix(in srgb, var(--c0) 8%, var(--w-panel)); } +.hk-open strong { color: var(--c0); } +.hk-shut { border-color: color-mix(in srgb, var(--c6) 60%, transparent); background: color-mix(in srgb, var(--c6) 8%, var(--w-panel)); } +.hk-shut strong { color: var(--c6); } +.hk-post { fill: #8fa9a0; } +.hk-open .hk-leaf { fill: var(--c0); } +.hk-shut .hk-leaf { fill: var(--c6); } +.hk-meters { display: grid; gap: 8px; margin: 8px 0; } +.hk-meter { --hc: var(--c0); } +.hk-meter-head { display: flex; justify-content: space-between; gap: 8px; margin: 0 0 3px; color: var(--w-muted); font: 0.74rem var(--w-mono); } +.hk-meter-head b { color: var(--hc); font-weight: 800; } +.hk-meter-track { position: relative; height: 18px; border-radius: 9px; background: #0b1210; border: 1px solid rgba(173, 201, 192, 0.18); overflow: hidden; } +.hk-meter-track i { position: absolute; inset: 0 auto 0 0; background: linear-gradient(90deg, color-mix(in srgb, var(--hc) 55%, transparent), var(--hc)); border-radius: 9px; transition: width 0.3s; } +.hk-meter-track em { position: absolute; top: -2px; bottom: -2px; width: 3px; margin-left: -1px; background: #fff; } +.hk-meter-mark { margin: 2px 0 0; color: var(--w-subtle); font: 0.68rem var(--w-mono); } +.hk-ladder { list-style: none; margin: 8px 0; padding: 0; display: grid; gap: 4px; } +.hk-rung { --hc: var(--c0); display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 10px; border-radius: 10px; border: 1px solid color-mix(in srgb, var(--hc) 40%, transparent); background: color-mix(in srgb, var(--hc) 7%, var(--w-panel)); } +.hk-rung i { flex: none; width: 14px; height: 14px; border-radius: 50%; background: var(--hc); box-shadow: 0 0 10px var(--hc); } +.hk-rung strong { display: block; color: #fff; font: 700 0.82rem var(--w-mono); } +.hk-rung span { display: block; color: var(--w-muted); font: 0.7rem var(--w-mono); } +.hk-rung.hk-dim { opacity: 0.45; } +.hk-rung.hk-dim i { box-shadow: none; } +.hk-dial { display: block; width: min(220px, 70%); margin: 6px auto; } +.hk-dial-ring { fill: #050807; stroke: rgba(173, 201, 192, 0.2); stroke-width: 14; } +.hk-dial-arc { fill: none; stroke-width: 14; stroke-linecap: round; } +.hk-dial-big { font: 800 26px var(--w-mono); } +.hk-dial-small { fill: var(--w-muted); font: 12px var(--w-mono); } +.hk-comb { display: block; width: 100%; max-width: 520px; height: auto; margin: 6px auto; } +.hk-cell-base { fill: #0b1210; stroke: rgba(0, 255, 136, 0.28); stroke-width: 2; } +.hk-cell.hk-on .hk-cell-base { stroke: #fff; stroke-width: 3; } +.hk-cell[role="button"] { cursor: pointer; } +.hk-cell[role="button"]:focus-visible .hk-cell-base { stroke: var(--c1); stroke-width: 4; } +.hk-cell-text { fill: #fff; font: 700 13px var(--w-mono); pointer-events: none; paint-order: stroke; stroke: #020403; stroke-width: 3px; } +.hk-grid-box { max-width: 100%; overflow-x: auto; margin: 8px 0; } +.hk-grid { width: 100%; border-collapse: separate; border-spacing: 3px; font: 0.7rem var(--w-mono); } +.hk-grid th { color: var(--w-muted); font-weight: 600; text-align: left; padding: 2px 4px; overflow-wrap: anywhere; } +.hk-grid thead th { text-align: center; } +.hk-grid td { --hc: var(--c0); min-width: 24px; height: 30px; padding: 2px; border-radius: 6px; text-align: center; color: #fff; font-weight: 700; background: color-mix(in srgb, var(--hc) 70%, #020403); } +.hk-grid td.hk-dim { background: #0b1210; color: var(--w-subtle); font-weight: 400; } +.hk-taps { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; } +.hk-tap { --hc: var(--c0); display: grid; justify-items: start; gap: 1px; min-height: 44px; min-width: 44px; padding: 6px 10px; border-color: color-mix(in srgb, var(--hc) 55%, transparent); text-align: left; } +.hk-tap strong { font: 700 0.76rem var(--w-mono); color: #fff; } +.hk-tap span { font: 0.66rem var(--w-mono); color: var(--hc); } +.hk-tap[aria-pressed="true"] { background: color-mix(in srgb, var(--hc) 22%, var(--w-panel)); border-color: var(--hc); } +.hk-tap.hk-dim { opacity: 0.4; } +.hk-verdict { --hc: var(--c0); display: grid; gap: 2px; margin: 8px 0; padding: 10px 12px; border-radius: 12px; border: 2px solid var(--hc); background: color-mix(in srgb, var(--hc) 10%, var(--w-panel)); box-shadow: 0 0 18px color-mix(in srgb, var(--hc) 22%, transparent); } +.hk-verdict strong { color: var(--hc); font: 800 1.15rem var(--w-mono); overflow-wrap: anywhere; } +.hk-verdict span { color: var(--w-text); font: 0.8rem/1.4 var(--w-mono); } +.hk-two { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; } +@container (min-width: 700px) { .hk-two { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.dk-source a { min-height: 44px; } +.hk-pill.hk-on { outline: 2px solid var(--hc); outline-offset: 1px; } +.hk-pill.hk-big { font-size: 0.86rem; padding: 4px 10px; } +.hk-pill.hk-faint { opacity: 0.5; } +.hk-path { margin: 6px 0; padding: 6px 8px; border: 1px solid rgba(173, 201, 192, 0.16); border-radius: 10px; background: var(--w-panel); } +.hk-path-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; margin: 0; } +.hk-path-head code { color: #fff; font: 700 0.78rem var(--w-mono); overflow-wrap: anywhere; } +.hk-cap { margin: 4px 0 0; color: var(--w-muted); font: 0.74rem var(--w-mono); text-align: center; } +.dk-svg.hk-map, .dk-svg.hk-strip, .dk-svg.hk-plot { max-width: 760px; } +.dk-range { height: 44px; } +.dk-chip { min-width: 44px; } +.hk-grid tbody th { white-space: nowrap; overflow-wrap: normal; } +.hk-grid thead th { writing-mode: vertical-rl; transform: rotate(180deg); height: 78px; vertical-align: bottom; text-align: left; white-space: nowrap; padding: 2px 0; } +.hk-card .dk-panel, .hk-card .dk-note, .hk-card .dk-source { display: none; } diff --git a/apps/website/public/widgets/hive-kit.js b/apps/website/public/widgets/hive-kit.js new file mode 100644 index 0000000000..fae94b13dd --- /dev/null +++ b/apps/website/public/widgets/hive-kit.js @@ -0,0 +1,153 @@ +// hive-kit.js -- the drawing the queen-and-hive widgets share, on top of dsp-kit.js (sliders, chips, +// presets, the URL hash, stat tiles): a kanban board whose cards sit in the column a spec fn names, +// a review gate that is open or shut, meters, a ladder of rungs, a clock dial, comb cells, a grid of +// answers and a row of buttons you tap. It decides nothing about the hive: every column, colour, +// verdict and number a widget shows comes from that widget's ./logic.js, the fn bodies of one +// gHashTag/t27 specs/queen or specs/automation spec compiled to wasm. Every word is a SAY_ and every +// range a K_ constant of the widget's own spec (window.T27_WIDGET). Inputs stay in this tab and in +// the URL hash; nothing is sent. +import { COLORS, el, svgEl } from './dsp-kit.js' + +export * from './dsp-kit.js' + +const css = document.createElement('link') +css.rel = 'stylesheet' +css.href = new URL('./hive-kit.css', import.meta.url).href +document.head.appendChild(css) +// A share card (?card=1, the 1200 x 630 shot) shows the answer, not the sliders that set it. +if (/[?&]card=1\b/.test(location.search)) document.documentElement.classList.add('hk-card') + +/** A colour by index: the seven of the palette, then 7 white and 8 grey; anything else is grey. */ +const EXTRA = ['#ffffff', '#7d8f89'] +export const hue = (t) => (Number.isInteger(t) && t >= 0 && t < COLORS.length ? COLORS[t] : Number.isInteger(t) && t < COLORS.length + EXTRA.length ? EXTRA[t - COLORS.length] : EXTRA[1]) +const toneStyle = (t) => `--hc:${hue(t)}` + +/** The question the lesson opens with, over the widget. */ +export const ask = (text) => el('p', { class: 'hk-ask' }, text) + +/** A small coloured label. */ +export const pill = (text, tone, extra = '') => el('span', { class: `hk-pill ${extra}`, style: toneStyle(tone) }, text) + +/** + * A kanban board. cols: [{ name, tone }]; cards: [{ col, title, sub, tone, on }]. A card sits in + * the column its `col` names (a spec fn's answer); a column with no card shows an empty slot. + */ +export function board(cols, cards) { + return el('div', { class: 'hk-board', role: 'list' }, cols.map((c, i) => { + const mine = cards.filter((k) => k.col === i) + return el('div', { class: `hk-col ${mine.length ? 'hk-col-full' : ''}`, role: 'listitem', style: toneStyle(c.tone) }, + el('p', { class: 'hk-col-head' }, c.name, el('b', {}, String(mine.length))), + mine.length ? mine.map((k) => el('div', { class: `hk-card ${k.on ? 'hk-on' : ''}`, style: toneStyle(k.tone ?? c.tone) }, el('strong', {}, k.title), k.sub ? el('span', {}, k.sub) : null)) + : el('div', { class: 'hk-slot' })) + })) +} + +/** A review gate: open (green) or shut (red), with what it says and why. */ +export function gate(open, title, sub) { + const g = el('div', { class: `hk-gate ${open ? 'hk-open' : 'hk-shut'}`, role: 'status' }) + const svg = svgEl('svg', { viewBox: '0 0 120 80', class: 'hk-gate-art', 'aria-hidden': 'true' }) + svg.append(svgEl('rect', { x: 6, y: 6, width: 10, height: 68, rx: 3, class: 'hk-post' }), svgEl('rect', { x: 104, y: 6, width: 10, height: 68, rx: 3, class: 'hk-post' })) + const w = open ? 10 : 43 + svg.append(svgEl('rect', { x: 17, y: 12, width: w, height: 56, rx: 4, class: 'hk-leaf' }), svgEl('rect', { x: 103 - w, y: 12, width: w, height: 56, rx: 4, class: 'hk-leaf' })) + g.append(svg, el('div', {}, el('strong', {}, title), sub ? el('span', {}, sub) : null)) + return g +} + +/** Horizontal meters. items: [{ label, value, max, tone, text, mark, markLabel }]. */ +export function meters(items) { + return el('div', { class: 'hk-meters' }, items.map((m) => { + const pct = (v) => `${Math.max(0, Math.min(100, (Number(v) / (m.max || 1)) * 100))}%` + return el('div', { class: 'hk-meter', style: toneStyle(m.tone) }, + el('p', { class: 'hk-meter-head' }, el('span', {}, m.label), el('b', {}, m.text ?? String(m.value))), + el('div', { class: 'hk-meter-track' }, el('i', { style: `width:${pct(m.value)}` }), m.mark != null ? el('em', { style: `left:${pct(m.mark)}`, title: m.markLabel ?? '' }) : null), + m.markLabel ? el('p', { class: 'hk-meter-mark' }, m.markLabel) : null) + })) +} + +/** A ladder of rungs, top to bottom. rungs: [{ label, sub, tone, on }]; `on` lights the rung. */ +export function ladder(rungs) { + return el('ol', { class: 'hk-ladder' }, rungs.map((r) => el('li', { class: `hk-rung ${r.on ? 'hk-on' : 'hk-dim'}`, style: toneStyle(r.tone) }, + el('i', {}), el('div', {}, el('strong', {}, r.label), r.sub ? el('span', {}, r.sub) : null)))) +} + +/** + * A clock dial: a ring from 0 to max with the value as an arc and marks as ticks. o: { value, max, + * marks: [{ at, label, tone }], tone, big, small, label }. + */ +export function dial(o) { + const svg = svgEl('svg', { viewBox: '0 0 200 200', class: 'hk-dial', role: 'img', 'aria-label': o.label ?? '' }) + const r = 78, cx = 100, cy = 100 + const at = (v) => { + const a = (Math.max(0, Math.min(1, v / (o.max || 1))) * 2 - 0.5) * Math.PI + return [cx + r * Math.cos(a), cy + r * Math.sin(a)] + } + svg.append(svgEl('circle', { cx, cy, r, class: 'hk-dial-ring' })) + const t = Math.max(0, Math.min(0.9999, o.value / (o.max || 1))) + if (t > 0) { + const [x, y] = at(Math.min(o.value, o.max * 0.9999)) + svg.append(svgEl('path', { d: `M${cx},${cy - r} A${r},${r} 0 ${t > 0.5 ? 1 : 0} 1 ${x.toFixed(1)},${y.toFixed(1)}`, class: 'hk-dial-arc', stroke: hue(o.tone) })) + } + for (const m of o.marks ?? []) { + const [x1, y1] = at(m.at) + const [x2, y2] = [cx + (x1 - cx) * 1.18, cy + (y1 - cy) * 1.18] + svg.append(svgEl('line', { x1, y1, x2, y2, stroke: hue(m.tone), 'stroke-width': 4, 'stroke-linecap': 'round' })) + } + svg.append(svgEl('text', { x: cx, y: cy + 4, class: 'hk-dial-big', 'text-anchor': 'middle', fill: hue(o.tone) }, o.big ?? '')) + if (o.small) svg.append(svgEl('text', { x: cx, y: cy + 28, class: 'hk-dial-small', 'text-anchor': 'middle' }, o.small)) + return svg +} + +/** Comb cells in a row of hexagons. cells: [{ label, fill, alpha, on }]; onTap(i) makes them buttons. */ +export function comb(cells, onTap) { + const n = cells.length, s = 46, w = s * 1.732 + const svg = svgEl('svg', { viewBox: `0 0 ${w * Math.min(n, 5) + w / 2 + 8} ${Math.ceil(n / 5) * s * 1.5 + s * 0.6 + 8}`, class: 'hk-comb', role: 'img' }) + cells.forEach((c, i) => { + const row = Math.floor(i / 5), col = i % 5 + const x = 4 + w / 2 + col * w + (row % 2 ? w / 2 : 0), y = 4 + s + row * s * 1.5 + const pts = [...Array(6)].map((_, k) => { const a = (Math.PI / 3) * k + Math.PI / 6; return `${(x + (s - 3) * Math.cos(a)).toFixed(1)},${(y + (s - 3) * Math.sin(a)).toFixed(1)}` }).join(' ') + const g = svgEl('g', { class: `hk-cell ${c.on ? 'hk-on' : ''}` }) + g.append(svgEl('polygon', { points: pts, class: 'hk-cell-base' })) + if (c.fill) g.append(svgEl('polygon', { points: pts, fill: c.fill, opacity: c.alpha ?? 1, class: 'hk-cell-glow' })) + g.append(svgEl('text', { x, y: y + 5, 'text-anchor': 'middle', class: 'hk-cell-text' }, c.label ?? '')) + if (onTap) { + g.setAttribute('role', 'button') + g.setAttribute('tabindex', '0') + g.setAttribute('aria-label', c.label ?? String(i)) + g.addEventListener('click', () => onTap(i)) + g.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); onTap(i) } }) + } + svg.append(g) + }) + return svg +} + +/** + * A grid of answers. o: { rows, cols, rowLabels, colLabels, cell(r, c) -> { text, tone, on } }. + * HTML, so it reflows; the row labels stay readable at 390 px. + */ +export function grid(o) { + const t = el('table', { class: 'hk-grid' }) + if (o.colLabels) t.append(el('thead', {}, el('tr', {}, el('th', {}), o.colLabels.map((c) => el('th', { scope: 'col' }, c))))) + t.append(el('tbody', {}, [...Array(o.rows)].map((_, r) => el('tr', {}, el('th', { scope: 'row' }, o.rowLabels?.[r] ?? ''), + [...Array(o.cols)].map((__, c) => { const v = o.cell(r, c); return el('td', { class: v.on ? 'hk-on' : 'hk-dim', style: toneStyle(v.tone) }, v.text ?? '') }))))) + return el('div', { class: 'hk-grid-box' }, t) +} + +/** A row of buttons. items: [{ label, sub, tone, pressed, disabled }]; onTap(i). */ +export function taps(items, onTap, label) { + return el('div', { class: 'hk-taps', role: 'group', 'aria-label': label ?? '' }, items.map((it, i) => { + const b = el('button', { class: `t27-btn hk-tap ${it.disabled ? 'hk-dim' : ''}`, type: 'button', style: toneStyle(it.tone), 'aria-pressed': it.pressed == null ? null : String(!!it.pressed) }, el('strong', {}, it.label), it.sub ? el('span', {}, it.sub) : null) + if (it.disabled) b.setAttribute('aria-disabled', 'true') + b.addEventListener('click', () => onTap(i)) + return b + })) +} + +/** A big answer line: what the spec decided, in its colour. */ +export const verdict = (word, tone, sub) => el('div', { class: 'hk-verdict', style: toneStyle(tone), role: 'status' }, el('strong', {}, word), sub ? el('span', {}, sub) : null) + +/** A section heading inside a widget. */ +export const head = (text) => el('p', { class: 'dk-label hk-head' }, text) + +/** Two panels side by side on a wide screen, stacked on a phone. */ +export const two = (...kids) => el('div', { class: 'hk-two' }, kids) diff --git a/apps/website/public/widgets/hive-lifecycle/card.png b/apps/website/public/widgets/hive-lifecycle/card.png new file mode 100644 index 0000000000..37fa4f7e53 Binary files /dev/null and b/apps/website/public/widgets/hive-lifecycle/card.png differ diff --git a/apps/website/public/widgets/hive-lifecycle/hive-lifecycle.t27 b/apps/website/public/widgets/hive-lifecycle/hive-lifecycle.t27 new file mode 100644 index 0000000000..81835c78f4 --- /dev/null +++ b/apps/website/public/widgets/hive-lifecycle/hive-lifecycle.t27 @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-lifecycle.t27 -- A task's life: the Queen's state machine +; Source of truth for public/widgets/hive-lifecycle/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-lifecycle/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/dispatch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 7 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps events; the state becomes next_state(state, event). Every event +; button is next_state of the current state, so an illegal one (99) is greyed and names nothing. +; Presets jump to a state. + +; WHAT IT DOES NOT CLAIM: it does not run the supervisor: the events are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_lifecycle; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-lifecycle"; +pub const TITLE : str = "A task's life: the Queen's state machine"; +pub const DESCRIPTION : str = "Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out."; +pub const IMAGE_ALT : str = "Lifecycle card: the task is ENDED, highlighted among twelve state chips. Of sixteen event buttons, seven are lit, the verdicts accept, send back, escalate, wait, empty, criteria changed and floor passed, each naming the state it leads to; the other nine are grey as illegal."; +pub const HOOK : str = "A row has no status column: its state is read from four fields. This machine is the one that chooses, starts, ends, reviews and releases work."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is next_state, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 7 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STATES : u8 = 12; +pub const K_EVENTS : u8 = 16; +pub const K_ILLEGAL : u32 = 99; +pub const K_PRESET_STATE : [4]u8 = [3, 0, 6, 10]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Ended, awaiting a verdict", "A new candidate", "Sent back", "Reaped"]; +pub const SAY_ASK : str = "What can happen to a task once a bee has started on it?"; +pub const SAY_STATE_NAMES : [12]str = ["none", "running", "refused", "ended", "wait", "accept", "send back", "escalate", "empty", "stale", "reaped", "released"]; +pub const SAY_EVENT_NAMES : [16]str = ["start", "refuse", "end", "end of another turn", "reap a stall", "reap at boot", "verdict accept", "verdict send back", "verdict escalate", "verdict wait", "verdict empty", "criteria changed", "CI red under the ceiling", "CI red at the ceiling", "floor passed", "dispatch again"]; +pub const SAY_STATES : str = "states"; +pub const SAY_EVENTS : str = "events: tap one"; +pub const SAY_NOW : str = "now"; +pub const SAY_ILLEGAL : str = "illegal here"; +pub const SAY_TRAIL : str = "path so far"; +pub const SAY_LEGAL : str = "{0} of {1} events are legal here"; +pub const SAY_NOTE : str = "Accepted is done only until CI goes red: a red check under the send-back ceiling sends it back, at the ceiling it escalates. A released or reaped task can be dispatched again."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/dispatch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test twelve_states_sixteen_events { + assert K_STATES == 12; + assert K_EVENTS == 16; + assert K_ILLEGAL > K_STATES; +} + +test the_presets_are_states { + assert K_PRESET_STATE[0] == 3; + assert K_PRESET_STATE[3] < K_STATES; +} diff --git a/apps/website/public/widgets/hive-lifecycle/index.html b/apps/website/public/widgets/hive-lifecycle/index.html new file mode 100644 index 0000000000..016987c37b --- /dev/null +++ b/apps/website/public/widgets/hive-lifecycle/index.html @@ -0,0 +1,47 @@ + + + + + + +A task's life: the Queen's state machine · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

A task's life: the Queen's state machine

+

Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is next_state, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 7 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-lifecycle/logic.js b/apps/website/public/widgets/hive-lifecycle/logic.js new file mode 100644 index 0000000000..7c4582c022 --- /dev/null +++ b/apps/website/public/widgets/hive-lifecycle/logic.js @@ -0,0 +1,123 @@ +// Generated by scripts/t27-logic.mjs from dispatch.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: e71997bc00e5e3c6fffc1d92d0ea1e73c09326215533f7bdec6a27ed9fffd7fe +// Imported by scripts/widget-logic.mjs for public/widgets/hive-lifecycle/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABQwpgBH9/f38Bf2ABfwF/YAF/AGAAAX9gAn9/AX9gBn9/f39/fwF/YAV/f39/fwF/YAN/f38Bf2ACf38AYAV/f39/fwADGhkBAQIBAwEBAwQBAQUEBgcBBAMICQEAAQUFBAUBcAEFBQUDAQARBgkBfwFBgIDAAAsHyQMWBm1lbW9yeQIAF3QyN19pc19jcmFzaF9lbmRfcmVhc29uAAAZdDI3X2lzX25vX2RpZmZfZW5kX3JlYXNvbgABFnQyN19lbmRfcmVhc29uX3RvX25hbWUAAhd0MjdfaXNfdmFsaWRfZW5kX3JlYXNvbgADGXQyN193cml0ZXJfZW5mb3JjZXNfdGFibGUABCB0MjdfYWNjZXB0X3JlcXVpcmVzX3B1bGxfcmVxdWVzdAAEE3QyN19mb2xsb3d1cF9yb3VuZHMABRJ0Mjdfc3RhbGxfcmVhcGFibGUABhN0Mjdfb3duZXJzX3Blcl90ZXJtAAcPdDI3X2xlYXNlX3Rha2VuAAgYdDI3X2NvdW50c19hZ2FpbnN0X2lzc3VlAAkRdDI3X3JldHJ5X2FsbG93ZWQACg10MjdfcmV2aWV3X29mAAsNdDI3X2Nhbl9zdGFydAAMDXQyN19jaG9zZW5fb2YADQ10MjdfY29sdW1uX29mAA4adDI3X3JlbGVhc2VzX3JlYWRfYnlfcm91bmQADw50MjdfbmV4dF9zdGF0ZQAQC3QyN19fcmV0X2F0ABEJdDI3X19mcmVlABIKdDI3X19hbGxvYwAUCQoBAEEBCwQVGBcTCvoLGQcAIABBAkYLEgAgAEEHSUH7ACAAQf8AcXZxC1ABAX8CQAJAIABBBk0NAEESIQBByYDAgAAhAQwBCyAAQQN0IgEoAvSAwIAAIQAgASgC8IDAgAAhAQtBACABNgKEgsCAAEEAIAA2AoiCwIAACwcAIABBB0kLBABBAAsHACAAQQBHCwgAIABB+ABLCwQAQQELBwAgACABcgsHACAAQQFzCwcAIABBAkkLPQACQCAADQBBAg8LAkAgASAATw0AQQMPCwJAIAIgAEcNAEEAQQIgBBtBAiADQQFxGw8LQQJBASAFQQFLGwsHACAAIAFJCz0AIABBf3NBAXFBAkEDIAJBAXEiAhsgACABckEBcSIBGyIAIAAgAEEEQQUgBEEBcRsgA0EBcRsgAhsgARsLNQACQCAARQ0AIAJBAXFFDQAgAUEBcQ0AQQQPCwJAIABBA00NAEEFDwsgAEECdCgCwIHAgAALBABBAAu4AQEBf0HjACECAkACQAJAAkACQAJAAkACQCAADgwABQQGBgECAwIEBAQHC0ECQeMAIAFBAUYbQQEgARsPC0EGQQdB4wAgAUENRhsgAUEMRhsPC0EJQQtB4wAgAUEORhsgAUELRhsPC0EJQeMAIAFBC0YbDwtBAUHjACABQQ9GGw8LIAFBfmoiAUEETw0BIAFBAnQoAtCBwIAADwsgAUF6aiIBQQlPDQAgAUECdCgC4IHAgAAhAgsgAgsIAEGEgsCAAAsaACABIAAgAUEBIAFBAUsbQQAgARCTgICAAAvQAQECfyOAgICAAEEQayIFJICAgIAAIAUgA0EfcToADwJAAkBBICACQQRqIgJBASADdCIDIAIgA0sbIgNBf2pnayICQf//A3FBfWoiBkENTw0AIAFCASACrIanakF8aiAGQQJ0IgMoAsCCwIAANgIAIAMgATYCwILAgAAMAQsgAUIBQSAgA0GDgARqQRB2QX9qZ2siA61C//8Dg4anQRB0akF8aiADQf//A3FBAnQiAygC9ILAgAA2AgAgAyABNgL0gsCAAAsgBUEQaiSAgICAAAtnAgJ/AX4jgICAgABBEGsiASSAgICAAEEAIQIgAUEAOgAMAkBBAUUNAEEAKQOogcCAACIDpyAAQQEgAEEBSxtBAEEAIANCIIinKAIAEYCAgIAAgICAgAAhAgsgAUEQaiSAgICAACACC5UCAQR/I4CAgIAAQRBrIgQkgICAgAAgBCACQR9xOgAPQQAhBQJAQX8gAUEEaiIGIAYgAUkbIgFBASACdCICIAEgAksbIgFBf2pnIgJFDQACQAJAQSAgAmsiAkH//wNxQX1qIgZBDU8NAEIBIAKtQv//A4OGpyEHIAZBAnQiAigCwILAgAAiAUUNASACQcCCwIAAaiAHIAFqQXxqKAIANgIAIAEhBQwCCyABQYOABGpBEHYQloCAgAAhBQwBCyACQYyCwIAAaiEBAkAgAigCjILAgAAiAkH//wNxDQBBARCWgICAACICRQ0BIAEgAiAHajYCACACIQUMAQsgASACIAdqNgIAIAIhBQsgBEEQaiSAgICAACAFC2QBAn9CAUEgIABBf2pnayIArUL//wODhqchAQJAIABB//8DcUECdCICKAL0gsCAACIARQ0AIAJB9ILAgABqIAFBEHQgAGpBfGooAgA2AgAgAA8LQQAgAUAAIgBBEHQgAEF/RhsLHAAgAUEAIAQgBCACIAMgBCAEEJiAgIAAQQFxGwvgAQECfyOAgICAAEEQayIGJICAgIAAIAYgA0EfcToAD0F/IARBBGoiByAHIARJGyIEQQEgA3QiAyAEIANLGyEEAkACQAJAIAJBBGoiAiADIAIgA0sbIgNBf2pnIgJBb2pBDEsNACAEQX9qZyIDDQFBACEDDAILQgFBICADQYOABGpBEHZBf2pna61C//8Dg4anQgFBICAEQYOABGpBEHZBf2pna61C//8Dg4anRiEDDAELQgFBICACa61C//8Dg4anQgFBICADa61C//8Dg4anRiEDCyAGQRBqJICAgIAAIAMLC44CAQBBgIDAAAuEAnJlZnVzYWxfc3RhcnQAdGltZV90b2tlbl9saW1pdABib3VuZGFyeV9jb3JyZWN0AHVua25vd24AbW9kZWxfZW5kZWRfdHVybgBpbnZhbGlkX2VuZF9yZWFzb24AcHVzaF9mYWlsZWQAY3Jhc2hlZAA4ABAAEAAAAA4AEAAQAAAAaAAQAAcAAAAfABAAEAAAAAAAEAANAAAAXAAQAAsAAAAwABAABwAAAAAAAACwABAAAQAAAAIAAAADAAAABAAAAAIAAAAEAAAAAwAAAAMAAAADAAAAYwAAAAoAAAAKAAAABQAAAAYAAAAHAAAABAAAAAgAAAAJAAAAYwAAAGMAAAALAAAA"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +const MEM = W.memory; +const UTF8_IN = new TextEncoder(); +const UTF8_OUT = new TextDecoder(); +function str(v, held, at) { + if (typeof v !== "string") + throw new TypeError(`${at}: ${typeof v} is not a str`); + const bytes = UTF8_IN.encode(v); + const ptr = W.t27__alloc(bytes.length) >>> 0; + if (ptr === 0) + throw new RangeError(`${at}: no wasm memory for ${bytes.length} bytes`); + held.push(ptr, bytes.length); + new Uint8Array(MEM.buffer, ptr, bytes.length).set(bytes); + return [ptr, bytes.length]; +} +function strCall(call) { + const held = []; + try { + return call(held); + } finally { + for (let i = 0; i < held.length; i += 2) + W.t27__free(held[i], held[i + 1]); + } +} +function strOut() { + const at = W.t27__ret_at() >>> 0; + const slot = new DataView(MEM.buffer); + const ptr = slot.getUint32(at, true); + const len = slot.getUint32(at + 4, true); + return UTF8_OUT.decode(new Uint8Array(MEM.buffer, ptr, len)); +} +function nextState(state, event) { + return W.t27_next_state(int(state, 0, 4294967295, "next_state.state"), int(event, 0, 4294967295, "next_state.event")) >>> 0; +} +function releasesReadByRound(stored) { + return W.t27_releases_read_by_round(int(stored, 0, 4294967295, "releases_read_by_round.stored")) >>> 0; +} +function columnOf(claim, issueOpen, issueListKnown) { + return W.t27_column_of(int(claim, 0, 4294967295, "column_of.claim"), issueOpen ? 1 : 0, issueListKnown ? 1 : 0) >>> 0; +} +function chosenOf(e0, e1, e2, e3, e4) { + return W.t27_chosen_of(e0 ? 1 : 0, e1 ? 1 : 0, e2 ? 1 : 0, e3 ? 1 : 0, e4 ? 1 : 0) >>> 0; +} +function canStart(running, limit) { + return (W.t27_can_start(int(running, 0, 4294967295, "can_start.running"), int(limit, 0, 4294967295, "can_start.limit")) & 1) !== 0; +} +function reviewOf(total, judged, met, committedKnown, committed, priorSendBacks) { + return W.t27_review_of(int(total, 0, 4294967295, "review_of.total"), int(judged, 0, 4294967295, "review_of.judged"), int(met, 0, 4294967295, "review_of.met"), committedKnown ? 1 : 0, int(committed, 0, 4294967295, "review_of.committed"), int(priorSendBacks, 0, 4294967295, "review_of.prior_send_backs")) >>> 0; +} +function retryAllowed(realAttempts) { + return (W.t27_retry_allowed(int(realAttempts, 0, 4294967295, "retry_allowed.real_attempts")) & 1) !== 0; +} +function countsAgainstIssue(interrupted) { + return (W.t27_counts_against_issue(interrupted ? 1 : 0) & 1) !== 0; +} +function leaseTaken(expired, sameHolder) { + return (W.t27_lease_taken(expired ? 1 : 0, sameHolder ? 1 : 0) & 1) !== 0; +} +function ownersPerTerm() { + return W.t27_owners_per_term() >>> 0; +} +function stallReapable(ageMinutes) { + return (W.t27_stall_reapable(int(ageMinutes, 0, 4294967295, "stall_reapable.age_minutes")) & 1) !== 0; +} +function followupRounds(requestsDuringRound) { + return W.t27_followup_rounds(int(requestsDuringRound, 0, 4294967295, "followup_rounds.requests_during_round")) >>> 0; +} +function acceptRequiresPullRequest() { + return (W.t27_accept_requires_pull_request() & 1) !== 0; +} +function writerEnforcesTable() { + return (W.t27_writer_enforces_table() & 1) !== 0; +} +function isValidEndReason(reason) { + return (W.t27_is_valid_end_reason(int(reason, 0, 4294967295, "is_valid_end_reason.reason")) & 1) !== 0; +} +function endReasonToName(reason) { + W.t27_end_reason_to_name(int(reason, 0, 4294967295, "end_reason_to_name.reason")); + return strOut(); +} +function isNoDiffEndReason(reason) { + return (W.t27_is_no_diff_end_reason(int(reason, 0, 4294967295, "is_no_diff_end_reason.reason")) & 1) !== 0; +} +function isCrashEndReason(reason) { + return (W.t27_is_crash_end_reason(int(reason, 0, 4294967295, "is_crash_end_reason.reason")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze(["claim_of", "fence_after"]); +export { + __NOT_LOWERED__, + acceptRequiresPullRequest, + canStart, + chosenOf, + columnOf, + countsAgainstIssue, + endReasonToName, + followupRounds, + isCrashEndReason, + isNoDiffEndReason, + isValidEndReason, + leaseTaken, + nextState, + ownersPerTerm, + releasesReadByRound, + retryAllowed, + reviewOf, + stallReapable, + writerEnforcesTable +}; diff --git a/apps/website/public/widgets/hive-lifecycle/logic.t27 b/apps/website/public/widgets/hive-lifecycle/logic.t27 new file mode 100644 index 0000000000..42911bfbb6 --- /dev/null +++ b/apps/website/public/widgets/hive-lifecycle/logic.t27 @@ -0,0 +1,594 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/dispatch.t27 -- the Queen's task lifecycle: what happens to an issue from candidate to done +; S09 of gHashTag/trinity#988 (gHashTag/t27#4828, re-filed from #3571). Four "Queen cycles" are written +; down in this family of repositories and only one of them moves work: the round of the trios supervisor +; (gHashTag/BrowserOS, trios/agent-server) that chooses an issue, starts a worker ("bee") on it, ends it, +; reviews it and releases or keeps it. This file is that lifecycle, made the canonical state machine, with +; the other cycles named as adapters and their gaps stated. Every decision below is a function the C +; backend executes, replayed against what the supervisor's own code did (tools/trinity_queen_dispatch.py): +; ts its TypeScript, imported and called under bun: the claim of a row over 2112 inputs, the +; public board's column, the single-flight round gate +; queend its Swift policy binary, built at the pin and asked choose, review and retry +; pg PostgreSQL with its own migrations: the singleton lease under 32 contenders, the dispatch +; writers on real rows, the criteria release, the CI take-back, the round's own SELECT +; (17.7 at this pin; the run at the old pin c25e1b02 used 16.15) +; lotus gHashTag/trinity's local Zig cycle compiled and its 29 tests run (at the trinity pin, which +; this re-pin does not move; measured with Zig 0.15.2 in the run at the old supervisor pin +; c25e1b02, not re-run here: Zig 0.15.2 was not on the re-pin host) +; live one snapshot of the public endpoints of the deployed supervisor +; Re-pinned for gHashTag/t27#6368 from feat/queen-supervisor @ c25e1b02 (branch deleted) to the branch +; production deploys. Between the pins every ts, queend and pg result is unchanged; what moved is read +; from the source and named below: runner lanes (a claim in state queued that takes no worker slot, +; a runner-only pass past the worker limit, leases and a 10-minute silence reaper for claimed bees) +; and the public card's verdict and judgedHead. +; Not measured: GitHub itself, a real provider or bee, the Mac app's own loop, Railway's deploy of a +; revision other than the pin. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenDispatch; + +pub const KIND : str = "queen-dispatch"; +pub const ID : str = "queen/dispatch"; +pub const NAME : str = "The Queen's task lifecycle, from candidate to done"; +pub const SUPERVISOR_REPO : str = "gHashTag/BrowserOS"; +pub const SUPERVISOR_BRANCH : str = "fix/queen-worker-provider-and-prompt-size"; +pub const SUPERVISOR_PIN : str = "879799a8bfb286e2ba8dbe840f0bcb81792d19ef"; +pub const CONSUMER_REPO : str = "gHashTag/trinity"; +pub const PINNED_REVISION : str = "afc9d38435ad01c48af6c33c311da422c38f046e"; +pub const ISSUE_BASELINE : str = "03ae2f93f5af2fd4fa23e4c613a3c0e19ac51530"; +pub const SOURCE_LINES : str = "trios/agent-server/apps/server/src/api/services/queen-tick.ts: stateOfDispatch :704-797, dispatchRowState :809-827, round :1459-1976, round SELECT :1645-1674, ceiling read :1722, runner-only pass :1909-1960, releaseStaleContracts :2717-2755, review sweep :2757-2846; queen-dispatch.ts: finishDispatch :4302-4334, reapers :4453-4550, recordDispatch :5175-5345; queen-runner-work.ts: reapSilentRunners :771-795; queen-lease.ts :66-136; queen-ci-verdict.ts :147-229; routes/queen-kanban.ts: columns :107-122, columnFor :179-193, dispatchColumn :333-347, board SELECT :494-507; queen-core/Sources/queend/main.swift: choose :181-334, review :336-379; QueenCore/QueenReviewDecision.swift :41-88; QueenCore/QueenRetryPolicy.swift :60-149"; +; --- The decision: one canonical machine, the others are adapters --------------------------------- +pub const CANONICAL : str = "the task lifecycle of the trios supervisor round: the only cycle that chooses, starts, ends, reviews and releases work, and the only one that runs (the deployed service answered working at the live snapshot)"; +pub const ADAPTERS : [5]str = [ + "trios-round: the canonical machine itself, implemented in queen-tick.ts, queen-dispatch.ts and queend", + "lotus-policy: gHashTag/trinity src/tri/queen/lotus_cycle.zig, a local loop that tunes a resource policy from episodes (Observe, Evaluate, Plan, Act, and an episode record); it holds no task and leases nothing; specs/queen/lotus.t27 is its design", + "agents-md-phases: Plan, Assign, Run, Test, Verdict, Evolve in gHashTag/trinity AGENTS.md, documented as tri queen lotus --phase and implemented nowhere; read as names for choose, start, run, review, verdict and nothing", + "ael-v2: OBSERVE, PLAN, DELEGATE, VERIFY, SYNTHESIZE, LEARN in the CLAUDE.md files, the loop an agent session follows; a session is not a task", + "phi-loop: the nine-phase development workflow of the CLAUDE.md files and gHashTag/trinity src/vibeec/phi_loop_cli.zig; a way of making a change, not a task's state; not measured here" +]; +pub const MIGRATION : str = "no runtime is renamed or removed: the adapters keep their words, and a claim about task state is made only in this file's vocabulary; a phase name from another cycle is translated by PHASE_MAP, never taken as a task state"; +pub const PHASE_MAP : [6]str = ["Plan -> choose", "Assign -> start (RUNNING)", "Run -> RUNNING", "Test -> review (criteria judged)", "Verdict -> ACCEPT, SENDBACK, ESCALATE, WAIT or EMPTY", "Evolve -> nothing in the canonical machine; episodes are lotus-policy's"]; +; --- States of a dispatch row, and the events that move it ---------------------------------------- +pub const S_NONE : u32 = 0; +pub const S_RUNNING : u32 = 1; +pub const S_REFUSED : u32 = 2; +pub const S_ENDED : u32 = 3; +pub const S_WAIT : u32 = 4; +pub const S_ACCEPT : u32 = 5; +pub const S_SENDBACK : u32 = 6; +pub const S_ESCALATE : u32 = 7; +pub const S_EMPTY : u32 = 8; +pub const S_STALE : u32 = 9; +pub const S_REAPED : u32 = 10; +pub const S_RELEASED : u32 = 11; +pub const ILLEGAL : u32 = 99; +pub const E_START : u32 = 0; +pub const E_REFUSE : u32 = 1; +pub const E_END : u32 = 2; +pub const E_END_OTHER_TURN : u32 = 3; +pub const E_REAP_STALL : u32 = 4; +pub const E_REAP_BOOT : u32 = 5; +pub const E_VERDICT_ACCEPT : u32 = 6; +pub const E_VERDICT_SENDBACK : u32 = 7; +pub const E_VERDICT_ESCALATE : u32 = 8; +pub const E_VERDICT_WAIT : u32 = 9; +pub const E_VERDICT_EMPTY : u32 = 10; +pub const E_CRITERIA_CHANGED : u32 = 11; +pub const E_CI_RED_UNDER_CEILING : u32 = 12; +pub const E_CI_RED_AT_CEILING : u32 = 13; +pub const E_FLOOR_PASSED : u32 = 14; +pub const E_REDISPATCH : u32 = 15; +pub const STATE_RULE : str = "a row has no status column: its state is read from started, finished_at, outcome and review_state; the claim it exerts on its issue is read from the state and three clocks (claim_of below)"; +pub const GUARD_RULE : str = "the guards are the WHERE clauses of the writers: a verdict reaches only a finished, unreaped row on an open issue whose verdict is none or wait; a criteria rewrite releases only finished, unreaped rows not accepted or already stale; a red required check takes back only an accept; an ending closes only an unfinished row of the same conversation"; +pub const WRITER_RULE : str = "the writer that starts a dispatch does not check the table: recordDispatch upserts on the issue key, archives the old row and replaces it whatever its state; the table is enforced by the choice (queend skips live claims), so a second path into the writer -- POST /queen/lease/tick, which skips the round gate -- is not covered by it"; +; --- Claims: what a row costs its issue ------------------------------------------------------------ +pub const V_NONE : u32 = 0; +pub const V_BLANK : u32 = 1; +pub const V_WAIT : u32 = 2; +pub const V_EMPTY : u32 = 3; +pub const V_SENDBACK : u32 = 4; +pub const V_ESCALATE : u32 = 5; +pub const V_ACCEPT : u32 = 6; +pub const V_FAILED : u32 = 7; +pub const V_CANCELLED : u32 = 8; +pub const V_STALE : u32 = 9; +pub const V_OTHER : u32 = 10; +pub const C_RUNNING : u32 = 0; +pub const C_ACCEPTED : u32 = 1; +pub const C_REJECTED : u32 = 2; +pub const C_AWAITING_REVIEW : u32 = 3; +pub const C_FAILED : u32 = 4; +pub const EMPTY_FLOOR_MS : u64 = 1800000; +pub const SENDBACK_FLOOR_MS : u64 = 3600000; +pub const WAIT_FLOOR_MS : u64 = 21600000; +pub const CEILING_RELEASE_MS : u64 = 3600000; +pub const MAX_CEILING_RELEASES : u32 = 1; +pub const SENDBACK_CEILING : u32 = 2; +pub const CLAIM_RULE : str = "running holds the issue and its files (so does queued, a runner's task, which takes no container worker slot); accepted is done; rejected and awaitingReview hold the issue (awaitingReview holds its files for 48 hours); failed frees both; a row older than 7 days, refused, or reaped holds nothing because the round does not select it"; +; --- Choosing ---------------------------------------------------------------------------------------- +pub const CHOICE_RULE : str = "capacity, then money (only in apiMetered billing), then for each candidate in the order GitHub listed the open issues: skip a live claim, skip landed work, skip a body without a boundary, skip a boundary held by another task, and take the FIRST that survives; nothing ranks the candidates"; +pub const PRIORITY_RULE : str = "there is no priority: GitHub's listing order (newest first by default) decides; QueenSalience ranks a review queue nothing calls; specs/queen/task_analysis.t27 states a priority order that no runtime applies"; +pub const DEPENDENCY_RULE : str = "none: blocked-by, sub-issues, epics and depends-on text are not read when choosing"; +pub const CAPACITY_RULE : str = "another bee may start while the bees in state running are fewer than TRIOS_QUEEN_MAX_WORKERS (default 4); keys x lanes bound it further at dispatch, and memory and disk guards can still refuse; when the container is full and a runner is idle, a runner-only pass asks queend again with the running bees relabelled queued and offers up to RUNNER_PASS_LIMIT (20) more tasks a round to runners only"; +pub const DEFAULT_WORKER_LIMIT : u32 = 4; +pub const NONE_CHOSEN : u32 = 5; +pub const COL_BACKLOG : u32 = 0; +pub const COL_BLOCKED : u32 = 1; +pub const COL_RUNNING : u32 = 2; +pub const COL_REVIEW : u32 = 3; +pub const COL_DONE : u32 = 4; +pub const COL_DROPPED : u32 = 5; +; --- Leases ------------------------------------------------------------------------------------------ +pub const LEASE_RULE : str = "one Queen at a time: queen_lease is taken by one INSERT ... ON CONFLICT DO UPDATE ... WHERE expired OR same holder, which bumps a fence; TTL 180 s, heartbeat 60 s, released after each round by expiring it, never deleting it, so the fence never restarts; only the round's recordTick is fenced"; +pub const LEASE_TTL_SECONDS : u32 = 180; +pub const LEASE_HEARTBEAT_SECONDS : u32 = 60; +pub const TASK_LEASE_RULE : str = "a bee the container runs has no per-task lease and no heartbeat: it is reaped only by age, 120 minutes after dispatch, or at the supervisor's boot, and its turn has no wall-clock cap; a claimed bee is also reaped after 10 minutes without a claimed_at renewal, a volunteer runner's task by its lease (offer unclaimed 10 minutes, lease unrenewed 15, any task older than 360), and neither reaper nor the boot reaper touches a runner's lane"; +pub const STALL_MINUTES : u32 = 120; +; --- Review, retries, evidence, cancellation -------------------------------------------------------- +pub const R_ACCEPT : u32 = 0; +pub const R_SENDBACK : u32 = 1; +pub const R_ESCALATE : u32 = 2; +pub const R_WAIT : u32 = 3; +pub const REVIEW_RULE : str = "queend review: no criteria -> escalate; fewer verdicts than criteria -> wait; every criterion met -> accept when a file was committed, escalate when none was or the count is unknown; something unmet -> escalate after two send-backs, otherwise send back; the TypeScript can then turn an accept into escalate or wait (no compiler witness, reviewer missing or silent three rounds)"; +pub const RETRY_RULE : str = "two counted send-backs, then escalate (the review policy and the CI take-back agree on 2); three free attempts with nothing judgeable escalate; QueenRetryPolicy (two real attempts, interruptions not counted) is compiled into queend and never asked by the container"; +pub const EVIDENCE_RULE : str = "verified done is review_state accept: criteria judged met, at least one committed file, a reviewer's answer; no pull request, CI run or merge is required, and a red required check can take an accept back only when an open pull request exists"; +pub const BOARD_RULE : str = "the public board's done column means accepted or merged, but a finished dispatch on an issue no longer open is drawn as done whatever its verdict"; +pub const CANCEL_RULE : str = "none: no route aborts a running turn or drops a dispatch; a closed issue leaves review, but its bee runs until it ends or is reaped"; +; --- Events, persistence, idempotency, projections -------------------------------------------------- +pub const EVENT_RULE : str = "no event IDs and no event log: GitHub is polled each round, a bee's result is read from its stream, and the only history is queen_dispatch_history (whole-row archives before an overwrite) and queen_transcript (keyed by conversation and sequence)"; +pub const IDEMPOTENCY_RULE : str = "a second ending matches no row; an ending of an older turn matches no row; a second criteria release and a second CI take-back find nothing; requests during a round coalesce into one more round"; +pub const PUBLIC_FIELDS : [6]str = ["/queen/status swarmState", "/queen/status queue.state", "/queen/status workers capacity, active, idle", "/queen/public-board columns", "/queen/public-board cards: number, title, column, criteria, needs, verdict, judgedHead, and for empty cards end_reason", "/queen/public-board pulse: rounds, bees, verdicts, lastRoundAt, roundSeconds"]; +pub const PRIVATE_FIELDS : str = "owned paths, branches, conversations, transcripts, workers, holders, the refusal text, providers, keys and token counts stay on the authenticated /queen/board"; + +; --- End reasons for runs that produce no commits ----------------------------------------------------------- +; Every finished dispatch has exactly one end reason from this closed list. "unknown" is allowed but counted. +; The runtime records these reasons; the public board shows them on "empty" cards and counts them over 7 days. +pub const R_MODEL_ENDED_TURN : u32 = 0; // Model ended its turn without editing any files +pub const R_TIME_TOKEN_LIMIT : u32 = 1; // Hit time or token limit during execution +pub const R_CRASHED : u32 = 2; // Container or tool crashed (with exit code) +pub const R_BOUNDARY_CORRECT : u32 = 3; // Boundary file was already correct, no changes needed +pub const R_REFUSAL_START : u32 = 4; // Refusal before the run started (invalid task, missing resources) +pub const R_PUSH_FAILED : u32 = 5; // Push to remote failed after successful run +pub const R_UNKNOWN : u32 = 6; // Cause could not be determined by the runner + +pub const END_REASON_COUNT : u32 = 7; +pub const END_REASON_NAMES : [7]str = [ + "model_ended_turn", + "time_token_limit", + "crashed", + "boundary_correct", + "refusal_start", + "push_failed", + "unknown" +]; + +; --- Public board extension: show end reason on empty cards ----------------------------------------------- +; The public board's "empty" cards now include the end reason when available, enabling breakdown of +; the 56.0% empty-branch share by reason from public endpoints alone. +pub const PUBLIC_BOARD_EMPTY_REASON : str = "for empty cards, include end_reason field when available"; +pub const LOCAL_VERSUS_EXTERNAL : str = "local: gHashTag/trinity's Zig lotus-policy and its .trinity/queen files on one machine; external: the trios supervisor in a container on Railway, documented in trios/docs/public-swarm-state.md and reached over its public endpoints"; +; --- Agents and workers --------------------------------------------------------------------------------- +pub const AGENT_LETTERS : u32 = 27; +pub const AGENTS_RULE : str = "the 27 named agents of AGENTS.md are documentation roles; no code maps an issue to a letter; a bee is a provider-backed model turn on one key and lane, told it works exactly one issue; the number of container bees is bounded by keys, lanes and TRIOS_QUEEN_MAX_WORKERS, runners add their own lanes, and nothing is bounded by 27"; +; --- The specs this one sits beside ---------------------------------------------------------------------- +pub const NEIGHBOURS : [6]str = [ + "specs/queen/lotus.t27: the lotus-policy design; does not compile in C at master", + "specs/queen/task_analysis.t27: a priority order no runtime applies; fixed here to compile and to sort", + "specs/queen/brain_summaries.t27: does not compile in C at master", + "specs/brain/brain.t27: no function; does not compile in C at master", + "specs/brain/bus.t27: compiles, one test of a version constant", + "specs/brain/cognitive_loop.t27: compiles, one test of a version constant" +]; +; --- Findings: measured, the tag names the evidence ------------------------------------------------------ +pub const FINDINGS_COUNT : u32 = 15; +pub const FINDINGS : [15]str = [ + "f50 four cycles disagree and three move nothing: the documented tri queen lotus --phase flags exist nowhere, zig build has no lotus-cycle step, and the Zig cycle's Record Episode stage is a comment [lotus]", + "f51 of the six specs the issue names, four do not compile in C at master (lotus, task_analysis, brain_summaries, brain) and two compile with one trivial test each [compile]", + "f52 there is no priority and no dependency: the first eligible issue in GitHub's listing order is chosen, in every one of 120 orders [queend; source]", + "f53 the one-release bound of a spent retry ceiling never applies: neither the round nor the board selects ceiling_releases, so a stored 1 reads as 0 and the issue is handed back again every hour [pg; ts]", + "f54 the public board draws a closed issue's dispatch as done whatever its verdict -- escalate, send-back, wait or none [ts]", + "f55 accept requires criteria met, a commit and a reviewer, but no pull request, CI run or merge; without an open pull request a red check cannot take it back [queend; pg]", + "f56 one Queen at a time holds: 32 contenders, 20 rounds, one owner per term and a fence that only rises; only recordTick carries that fence [pg; source]", + "f57 the dispatch writer enforces nothing: it replaced a row still awaiting review; exclusivity is the issue key plus a choice that skips live claims [pg]", + "f58 a bee the container runs has no heartbeat: it is reaped by age alone, at 121 minutes and not at 119, or at boot, and its turn has no wall-clock cap; at this pin a claimed bee or a runner's task is also reaped by a missed renewal (read, not run) [pg; source]", + "f59 nothing cancels a running bee [source]", + "f60 two counted send-backs then escalate, in review and in the CI take-back alike; QueenRetryPolicy is compiled and never asked [queend; pg; source]", + "f61 a duplicate ending, a late ending of an older turn, a second criteria release and a second CI take-back change nothing; there are no event IDs [pg]", + "f62 the 27 named agents are documentation: no code maps an issue to a letter, and task_analysis.t27's WORKER_COUNT = 27 equated them with workers [source; spec]", + "f63 task_analysis.t27 sorted by returning its input; it now orders by score with tests on unsorted input, and is labelled as applied by no runtime [spec]", + "f64 the supervisor is a live service: at the snapshot (2026-10-05 05:59Z) it reported working with 18 of 70 workers and 1218 public cards [live]" +]; +pub const ENABLED : bool = true; + +; --- Rules ------------------------------------------------------------------------------------------------------ + +pub fn next_state(state: u32, event: u32) -> u32 { + if (state == S_NONE) { + if (event == E_START) { return S_RUNNING; } + if (event == E_REFUSE) { return S_REFUSED; } + return ILLEGAL; + } + if (state == S_RUNNING) { + if (event == E_END) { return S_ENDED; } + if (event == E_REAP_STALL) { return S_REAPED; } + if (event == E_REAP_BOOT) { return S_REAPED; } + return ILLEGAL; + } + if (state == S_ENDED or state == S_WAIT) { + if (event == E_VERDICT_ACCEPT) { return S_ACCEPT; } + if (event == E_VERDICT_SENDBACK) { return S_SENDBACK; } + if (event == E_VERDICT_ESCALATE) { return S_ESCALATE; } + if (event == E_VERDICT_WAIT) { return S_WAIT; } + if (event == E_VERDICT_EMPTY) { return S_EMPTY; } + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + if (event == E_FLOOR_PASSED) { return S_RELEASED; } + return ILLEGAL; + } + if (state == S_ACCEPT) { + if (event == E_CI_RED_UNDER_CEILING) { return S_SENDBACK; } + if (event == E_CI_RED_AT_CEILING) { return S_ESCALATE; } + return ILLEGAL; + } + if (state == S_SENDBACK or state == S_EMPTY) { + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + if (event == E_FLOOR_PASSED) { return S_RELEASED; } + return ILLEGAL; + } + if (state == S_ESCALATE) { + if (event == E_CRITERIA_CHANGED) { return S_STALE; } + return ILLEGAL; + } + if (state == S_STALE or state == S_REAPED or state == S_REFUSED or state == S_RELEASED) { + if (event == E_REDISPATCH) { return S_RUNNING; } + return ILLEGAL; + } + return ILLEGAL; +} + +pub fn claim_of(finished: bool, verdict: u32, idle_ms: u64, send_backs: u32, releases: u32) -> u32 { + if (finished == false) { return C_RUNNING; } + if (verdict == V_ACCEPT) { return C_ACCEPTED; } + if (verdict == V_FAILED or verdict == V_CANCELLED) { return C_FAILED; } + if (verdict == V_EMPTY) { + if (idle_ms >= EMPTY_FLOOR_MS) { return C_FAILED; } + return C_REJECTED; + } + if (verdict == V_STALE) { return C_FAILED; } + if (verdict == V_SENDBACK) { + if (idle_ms >= SENDBACK_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; } + if (releases >= MAX_CEILING_RELEASES) { return C_REJECTED; } + if (idle_ms >= CEILING_RELEASE_MS) { return C_FAILED; } + return C_REJECTED; + } + if (verdict == V_NONE or verdict == V_BLANK or verdict == V_WAIT) { + if (idle_ms >= WAIT_FLOOR_MS and send_backs < SENDBACK_CEILING) { return C_FAILED; } + } + return C_AWAITING_REVIEW; +} + +; What the round and the board actually pass for `releases`: neither selects the column. +pub fn releases_read_by_round(stored: u32) -> u32 { + return 0; +} + +pub fn column_of(claim: u32, issue_open: bool, issue_list_known: bool) -> u32 { + if (claim != C_RUNNING and issue_list_known and issue_open == false) { return COL_DONE; } + if (claim == C_RUNNING) { return COL_RUNNING; } + if (claim == C_ACCEPTED) { return COL_DONE; } + if (claim == C_REJECTED or claim == C_AWAITING_REVIEW) { return COL_REVIEW; } + return COL_DROPPED; +} + +pub fn chosen_of(e0: bool, e1: bool, e2: bool, e3: bool, e4: bool) -> u32 { + if (e0) { return 0; } + if (e1) { return 1; } + if (e2) { return 2; } + if (e3) { return 3; } + if (e4) { return 4; } + return NONE_CHOSEN; +} + +pub fn can_start(running: u32, limit: u32) -> bool { + return running < limit; +} + +pub fn review_of(total: u32, judged: u32, met: u32, committed_known: bool, committed: u32, prior_send_backs: u32) -> u32 { + if (total == 0) { return R_ESCALATE; } + if (judged < total) { return R_WAIT; } + if (met == total) { + if (committed_known and committed > 0) { return R_ACCEPT; } + return R_ESCALATE; + } + if (prior_send_backs >= SENDBACK_CEILING) { return R_ESCALATE; } + return R_SENDBACK; +} + +pub fn retry_allowed(real_attempts: u32) -> bool { + return real_attempts < SENDBACK_CEILING; +} + +pub fn counts_against_issue(interrupted: bool) -> bool { + return interrupted == false; +} + +pub fn lease_taken(expired: bool, same_holder: bool) -> bool { + return expired or same_holder; +} + +pub fn fence_after(fence: u64) -> u64 { + return fence + 1; +} + +pub fn owners_per_term() -> u32 { + return 1; +} + +pub fn stall_reapable(age_minutes: u32) -> bool { + return age_minutes > STALL_MINUTES; +} + +pub fn followup_rounds(requests_during_round: u32) -> u32 { + if (requests_during_round > 0) { return 1; } + return 0; +} + +pub fn accept_requires_pull_request() -> bool { + return false; +} + +pub fn writer_enforces_table() -> bool { + return false; +} + +invariant illegal_is_outside_every_state { assert ILLEGAL > S_RELEASED; } +invariant the_ceiling_is_two { assert SENDBACK_CEILING == 2 and MAX_CEILING_RELEASES == 1; } +invariant the_floors_rise { assert EMPTY_FLOOR_MS < SENDBACK_FLOOR_MS and SENDBACK_FLOOR_MS < WAIT_FLOOR_MS; } +invariant the_heartbeat_beats_inside_the_ttl { assert LEASE_HEARTBEAT_SECONDS * 3 == LEASE_TTL_SECONDS; } +invariant end_reason_count_matches_names { assert END_REASON_COUNT == END_REASON_NAMES.len(); } +invariant all_end_reasons_valid { + assert is_valid_end_reason(R_MODEL_ENDED_TURN); + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT); + assert is_valid_end_reason(R_CRASHED); + assert is_valid_end_reason(R_BOUNDARY_CORRECT); + assert is_valid_end_reason(R_REFUSAL_START); + assert is_valid_end_reason(R_PUSH_FAILED); + assert is_valid_end_reason(R_UNKNOWN); +} + +; --- End reason handling functions -------------------------------------------------------------------------- + +pub fn is_valid_end_reason(reason: u32) -> bool { + return reason < END_REASON_COUNT; +} + +pub fn end_reason_to_name(reason: u32) -> str { + if (is_valid_end_reason(reason)) { + return END_REASON_NAMES[reason]; + } + return "invalid_end_reason"; +} + +pub fn is_no_diff_end_reason(reason: u32) -> bool { + return reason == R_MODEL_ENDED_TURN + or reason == R_TIME_TOKEN_LIMIT + or reason == R_BOUNDARY_CORRECT + or reason == R_REFUSAL_START + or reason == R_PUSH_FAILED + or reason == R_UNKNOWN; +} + +pub fn is_crash_end_reason(reason: u32) -> bool { + return reason == R_CRASHED; +} + +test the_table_moves_only_along_its_guards { + assert next_state(S_NONE, E_START) == S_RUNNING; + assert next_state(S_RUNNING, E_END) == S_ENDED; + assert next_state(S_ENDED, E_END) == ILLEGAL; + assert next_state(S_RUNNING, E_VERDICT_ACCEPT) == ILLEGAL; + assert next_state(S_ACCEPT, E_CRITERIA_CHANGED) == ILLEGAL; + assert next_state(S_ACCEPT, E_CI_RED_AT_CEILING) == S_ESCALATE; + assert next_state(S_ESCALATE, E_FLOOR_PASSED) == ILLEGAL; + assert next_state(S_REAPED, E_CRITERIA_CHANGED) == ILLEGAL; + assert next_state(S_STALE, E_REDISPATCH) == S_RUNNING; + assert next_state(S_RUNNING, E_END_OTHER_TURN) == ILLEGAL; + // Every legal transition, once each: a guard no assert reaches can be dropped unnoticed (#9886). + assert next_state(S_NONE, E_REFUSE) == S_REFUSED; + assert next_state(S_NONE, E_END) == ILLEGAL; + assert next_state(S_RUNNING, E_REAP_STALL) == S_REAPED; + assert next_state(S_RUNNING, E_REAP_BOOT) == S_REAPED; + assert next_state(S_ENDED, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ENDED, E_VERDICT_SENDBACK) == S_SENDBACK; + assert next_state(S_ENDED, E_VERDICT_ESCALATE) == S_ESCALATE; + assert next_state(S_ENDED, E_VERDICT_WAIT) == S_WAIT; + assert next_state(S_ENDED, E_VERDICT_EMPTY) == S_EMPTY; + assert next_state(S_ENDED, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_ENDED, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_WAIT, E_VERDICT_ACCEPT) == S_ACCEPT; + assert next_state(S_ACCEPT, E_CI_RED_UNDER_CEILING) == S_SENDBACK; + assert next_state(S_SENDBACK, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_FLOOR_PASSED) == S_RELEASED; + assert next_state(S_EMPTY, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_SENDBACK, E_START) == ILLEGAL; + assert next_state(S_ESCALATE, E_CRITERIA_CHANGED) == S_STALE; + assert next_state(S_REAPED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_REFUSED, E_REDISPATCH) == S_RUNNING; + assert next_state(S_RELEASED, E_REDISPATCH) == S_RUNNING; +} + +test a_claim_follows_the_verdict_and_three_clocks { + assert claim_of(false, V_ACCEPT, 0, 0, 0) == C_RUNNING; + assert claim_of(true, V_ACCEPT, 0, 0, 0) == C_ACCEPTED; + assert claim_of(true, V_EMPTY, 1799999, 0, 0) == C_REJECTED; + assert claim_of(true, V_EMPTY, 1800000, 0, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 1, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 2, 1) == C_REJECTED; + assert claim_of(true, V_SENDBACK, 3600000, 2, releases_read_by_round(1)) == C_FAILED; + assert claim_of(true, V_WAIT, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_ESCALATE, 999999999, 0, 0) == C_AWAITING_REVIEW; + // #9886: each verdict that frees the issue, the send-back floor at its edge with a release + // already spent, and the wait floor's two edges. + assert claim_of(true, V_FAILED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_CANCELLED, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_STALE, 0, 0, 0) == C_FAILED; + assert claim_of(true, V_SENDBACK, 3600000, 1, 1) == C_FAILED; + assert claim_of(true, V_NONE, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_BLANK, 21600000, 0, 0) == C_FAILED; + assert claim_of(true, V_WAIT, 21599999, 0, 0) == C_AWAITING_REVIEW; + assert claim_of(true, V_WAIT, 21600000, 2, 0) == C_AWAITING_REVIEW; +} + +test a_closed_issue_is_drawn_done_whatever_its_verdict { + assert column_of(C_AWAITING_REVIEW, false, true) == COL_DONE; + assert column_of(C_AWAITING_REVIEW, true, true) == COL_REVIEW; + assert column_of(C_RUNNING, false, true) == COL_RUNNING; + assert column_of(C_FAILED, true, true) == COL_DROPPED; + assert column_of(C_ACCEPTED, true, true) == COL_DONE; +} + +test the_first_eligible_wins_in_any_order { + assert chosen_of(false, false, true, true, false) == 2; + assert chosen_of(false, true, false, false, true) == 1; + assert chosen_of(false, false, false, false, false) == NONE_CHOSEN; + assert chosen_of(true, true, false, false, false) == 0; + assert chosen_of(false, false, false, true, true) == 3; + assert chosen_of(false, false, false, false, true) == 4; + assert can_start(3, DEFAULT_WORKER_LIMIT) == true; + assert can_start(4, DEFAULT_WORKER_LIMIT) == false; +} + +test review_escalates_without_work_and_after_two_send_backs { + assert review_of(0, 0, 0, true, 1, 0) == R_ESCALATE; + assert review_of(2, 1, 1, true, 1, 0) == R_WAIT; + assert review_of(1, 1, 1, true, 1, 0) == R_ACCEPT; + assert review_of(1, 1, 1, true, 0, 0) == R_ESCALATE; + assert review_of(1, 1, 1, false, 0, 0) == R_ESCALATE; + assert review_of(1, 1, 0, true, 1, 1) == R_SENDBACK; + assert review_of(1, 1, 0, true, 1, 2) == R_ESCALATE; + assert retry_allowed(1) == true; + assert retry_allowed(2) == false; + assert counts_against_issue(true) == false; + assert counts_against_issue(false) == true; +} + +test one_lease_one_owner_and_age_alone_reaps { + assert lease_taken(false, false) == false; + assert lease_taken(true, false) == true; + assert lease_taken(false, true) == true; + assert fence_after(3) == 4; + assert owners_per_term() == 1; + assert stall_reapable(121) == true; + assert stall_reapable(119) == false; + // The comparison is strict: 120 is not reapable. f58 measured 119 and 121, not 120 itself. + assert stall_reapable(120) == false; + assert followup_rounds(5) == 1; + assert followup_rounds(0) == 0; + assert accept_requires_pull_request() == false; + assert writer_enforces_table() == false; +} + +; --- End reason tests -------------------------------------------------------------------------------------- + +test end_reason_constants_are_correct { + assert R_MODEL_ENDED_TURN == 0; + assert R_TIME_TOKEN_LIMIT == 1; + assert R_CRASHED == 2; + assert R_BOUNDARY_CORRECT == 3; + assert R_REFUSAL_START == 4; + assert R_PUSH_FAILED == 5; + assert R_UNKNOWN == 6; + assert END_REASON_COUNT == 7; + // The lines above fold at comptime, so this test ran 0 runtime asserts (#9886); these two + // call the functions that read the constants: the last reason is named, one past it is not. + assert end_reason_to_name(END_REASON_COUNT - 1) == "unknown"; + assert is_valid_end_reason(END_REASON_COUNT) == false; +} + +test end_reason_names_array_matches_constants { + assert END_REASON_NAMES[0] == "model_ended_turn"; + assert END_REASON_NAMES[1] == "time_token_limit"; + assert END_REASON_NAMES[2] == "crashed"; + assert END_REASON_NAMES[3] == "boundary_correct"; + assert END_REASON_NAMES[4] == "refusal_start"; + assert END_REASON_NAMES[5] == "push_failed"; + assert END_REASON_NAMES[6] == "unknown"; +} + +test end_reason_validation_works { + assert is_valid_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_valid_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_valid_end_reason(R_CRASHED) == true; + assert is_valid_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_valid_end_reason(R_REFUSAL_START) == true; + assert is_valid_end_reason(R_PUSH_FAILED) == true; + assert is_valid_end_reason(R_UNKNOWN) == true; + assert is_valid_end_reason(7) == false; // One past the last valid reason + assert is_valid_end_reason(99) == false; // Way past the last valid reason +} + +test end_reason_to_name_conversion { + assert end_reason_to_name(R_MODEL_ENDED_TURN) == "model_ended_turn"; + assert end_reason_to_name(R_TIME_TOKEN_LIMIT) == "time_token_limit"; + assert end_reason_to_name(R_CRASHED) == "crashed"; + assert end_reason_to_name(R_BOUNDARY_CORRECT) == "boundary_correct"; + assert end_reason_to_name(R_REFUSAL_START) == "refusal_start"; + assert end_reason_to_name(R_PUSH_FAILED) == "push_failed"; + assert end_reason_to_name(R_UNKNOWN) == "unknown"; + assert end_reason_to_name(99) == "invalid_end_reason"; // Invalid reason returns special string +} + +test no_diff_end_reason_classification { + assert is_no_diff_end_reason(R_MODEL_ENDED_TURN) == true; + assert is_no_diff_end_reason(R_TIME_TOKEN_LIMIT) == true; + assert is_no_diff_end_reason(R_BOUNDARY_CORRECT) == true; + assert is_no_diff_end_reason(R_REFUSAL_START) == true; + assert is_no_diff_end_reason(R_PUSH_FAILED) == true; + assert is_no_diff_end_reason(R_UNKNOWN) == true; + assert is_no_diff_end_reason(R_CRASHED) == false; // Crashes are separate category +} + +test crash_end_reason_classification { + assert is_crash_end_reason(R_CRASHED) == true; + assert is_crash_end_reason(R_MODEL_ENDED_TURN) == false; + assert is_crash_end_reason(R_TIME_TOKEN_LIMIT) == false; + assert is_crash_end_reason(R_BOUNDARY_CORRECT) == false; + assert is_crash_end_reason(R_REFUSAL_START) == false; + assert is_crash_end_reason(R_PUSH_FAILED) == false; + assert is_crash_end_reason(R_UNKNOWN) == false; +} + +test end_reason_enables_breakdown_of_empty_branches { + // This test validates that the end reason vocabulary enables breaking down + // the 56.0% empty-branch share by reason from public endpoints alone + + // Simulate counts for empty branches by reason + var model_ended_count : u32 = 100; + var token_limit_count : u32 = 80; + var boundary_correct_count : u32 = 60; + var refusal_start_count : u32 = 40; + var push_failed_count : u32 = 30; + var unknown_count : u32 = 7; // Should be < 10% of total + var crash_count : u32 = 0; + + var total_empty : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count + crash_count; + + // Verify unknown is less than 10% of total (success criterion from issue) + assert unknown_count < total_empty / 10; + + // Verify we can calculate breakdown percentages + var model_percentage : u32 = (model_ended_count * 100) / total_empty; + var token_percentage : u32 = (token_limit_count * 100) / total_empty; + var unknown_percentage : u32 = (unknown_count * 100) / total_empty; + + assert model_percentage > 0; + assert token_percentage > 0; + assert unknown_percentage < 10; // Unknown should be < 10% + + // Verify no-diff vs crash categorization works + var no_diff_total : u32 = model_ended_count + token_limit_count + boundary_correct_count + + refusal_start_count + push_failed_count + unknown_count; + var crash_total : u32 = crash_count; + + assert no_diff_total == total_empty - crash_total; +} diff --git a/apps/website/public/widgets/hive-lifecycle/tool.js b/apps/website/public/widgets/hive-lifecycle/tool.js new file mode 100644 index 0000000000..e882ee7e51 --- /dev/null +++ b/apps/website/public/widgets/hive-lifecycle/tool.js @@ -0,0 +1,29 @@ +// hive-lifecycle/tool.js -- lesson 7 of queen-and-hive: a task's life. The state after a tap, and +// whether each event button is lit or greyed, are next_state of gHashTag/t27 specs/queen/dispatch.t27, +// compiled to wasm (./logic.js); an event it answers ILLEGAL for is greyed. Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-lifecycle.t27. +import { nextState } from './logic.js' +import { K, W, ask, controls, el, fill, head, pill, root, say, source, taps } from '../hive-kit.js' + +const NS = K('K_STATES', 12), NE = K('K_EVENTS', 16), ILLEGAL = K('K_ILLEGAL', 99) +const STATE_TONES = [8, 0, 6, 2, 1, 0, 4, 3, 8, 5, 6, 2] +let trail = [] +const ctl = controls([{ key: 'st', kind: 'hidden', min: 0, max: NS - 1 }], + { names: W.SAY_PRESET_NAMES, values: { st: K('K_PRESET_STATE', []) } }, (s) => { if (!trail.length || trail.at(-1) !== s.st) trail = [...trail.slice(-5), s.st]; draw(s) }) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const next = [...Array(NE)].map((_, e) => nextState(s.st, e)) + const legal = next.filter((v) => v !== ILLEGAL).length + out.replaceChildren( + head(say('SAY_STATES')), + el('p', { class: 'hk-taps hk-states' }, [...Array(NS)].map((_, i) => pill(i === s.st ? `${say('SAY_NOW')}: ${say('SAY_STATE_NAMES', i)}` : say('SAY_STATE_NAMES', i), STATE_TONES[i], i === s.st ? 'hk-on hk-big' : 'hk-faint'))), + head(`${say('SAY_EVENTS')} · ${fill(say('SAY_LEGAL'), legal, NE)}`), + taps(next.map((v, e) => ({ label: say('SAY_EVENT_NAMES', e), sub: v === ILLEGAL ? say('SAY_ILLEGAL') : `→ ${say('SAY_STATE_NAMES', v)}`, tone: v === ILLEGAL ? 8 : STATE_TONES[v], disabled: v === ILLEGAL })), (e) => { if (next[e] !== ILLEGAL) ctl.set('st', next[e]) }, say('SAY_EVENTS')), + head(say('SAY_TRAIL')), + el('p', { class: 'hk-taps' }, trail.map((t) => pill(say('SAY_STATE_NAMES', t), STATE_TONES[t])))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-master-red/card.png b/apps/website/public/widgets/hive-master-red/card.png new file mode 100644 index 0000000000..d90fe27965 Binary files /dev/null and b/apps/website/public/widgets/hive-master-red/card.png differ diff --git a/apps/website/public/widgets/hive-master-red/hive-master-red.t27 b/apps/website/public/widgets/hive-master-red/hive-master-red.t27 new file mode 100644 index 0000000000..50aa86825e --- /dev/null +++ b/apps/website/public/widgets/hive-master-red/hive-master-red.t27 @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-master-red.t27 -- Inherited red: what master's runs say about a red check +; Source of truth for public/widgets/hive-master-red/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-master-red/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 15 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps each of four master runs to change its result and whether it ran at +; or after the PR's merge base, and whether the bee named the check. Each rung is run_says of its +; run and the running look_back from MASTER_UNKNOWN; the gate is discount_holds(bee named, the +; result). + +; WHAT IT DOES NOT CLAIM: it does not read GitHub Actions: the runs are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_master_red; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-master-red"; +pub const TITLE : str = "Inherited red: what master's runs say about a red check"; +pub const DESCRIPTION : str = "The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts."; +pub const IMAGE_ALT : str = "Inherited red card: four master runs, newest first: cancelled, failure, success, failure, all at or after the merge base. The top rung says unknown and is looked past, the next says red and decides; the look-back reads MASTER RED and the discount holds, green."; +pub const HOOK : str = "On 2026-10-06 a PR merged red: master's run was cancelled and read as red. A cancelled run says nothing about the code; only a completed failure counts."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is run_says, look_back and discount_holds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 15 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_RUNS : u8 = 4; +pub const K_RUN_KINDS : u8 = 8; +pub const K_MASTER_UNKNOWN : u8 = 0; +pub const K_PRESET_R0 : [4]u8 = [3, 3, 3, 1]; +pub const K_PRESET_R1 : [4]u8 = [1, 3, 1, 1]; +pub const K_PRESET_R2 : [4]u8 = [2, 3, 2, 2]; +pub const K_PRESET_R3 : [4]u8 = [1, 3, 1, 1]; +pub const K_PRESET_BASE : [4]u8 = [15, 15, 1, 15]; +pub const K_PRESET_NAMED : [4]u8 = [1, 1, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Cancelled, then a failure", "Only cancelled runs (#6956)", "Failure before the merge base", "Bee did not name it"]; +pub const SAY_ASK : str = "The bee says this red check is red on master too. Is it?"; +pub const SAY_RUN_NAMES : [8]str = ["missing", "failure", "success", "cancelled", "skipped", "timed out", "running", "other"]; +pub const SAY_MASTER_NAMES : [3]str = ["UNKNOWN", "MASTER RED", "MASTER GREEN"]; +pub const SAY_RUN : str = "master run {0}, newest first"; +pub const SAY_AFTER : str = "at or after the merge base"; +pub const SAY_BEFORE : str = "before the merge base"; +pub const SAY_TAP : str = "tap the result to change it"; +pub const SAY_NAMED : str = "the bee named this check"; +pub const SAY_SAYS : str = "says"; +pub const SAY_SO_FAR : str = "so far"; +pub const SAY_HOLDS : str = "The discount holds: the red is inherited"; +pub const SAY_FAILS : str = "No discount: the red blocks"; +pub const SAY_NOTE : str = "The newest decisive run wins: a cancelled newest run is looked past, a success is not. A run older than the merge base is not evidence about the code the PR started from."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/merger_gate.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_runs_eight_kinds { + assert K_RUNS == 4; + assert K_RUN_KINDS == 8; + assert K_MASTER_UNKNOWN == 0; +} + +test the_incident_preset_is_all_cancelled { + assert K_PRESET_R0[1] == 3; + assert K_PRESET_R3[1] == 3; + assert K_PRESET_BASE[0] == 15; +} diff --git a/apps/website/public/widgets/hive-master-red/index.html b/apps/website/public/widgets/hive-master-red/index.html new file mode 100644 index 0000000000..500c436698 --- /dev/null +++ b/apps/website/public/widgets/hive-master-red/index.html @@ -0,0 +1,47 @@ + + + + + + +Inherited red: what master's runs say about a red check · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Inherited red: what master's runs say about a red check

+

The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is run_says, look_back and discount_holds, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 15 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-master-red/logic.js b/apps/website/public/widgets/hive-master-red/logic.js new file mode 100644 index 0000000000..0ccced68f4 --- /dev/null +++ b/apps/website/public/widgets/hive-master-red/logic.js @@ -0,0 +1,52 @@ +// Generated by scripts/t27-logic.mjs from merger_gate.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 7390058c25153a807bd1dfc78aa132c6905779b69ab46edd6d9d383d15a74c5f +// Imported by scripts/widget-logic.mjs for public/widgets/hive-master-red/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHARgA39/fwF/YAJ/fwF/YAF/AX9gBX9/f39/AX8DCQgAAQACAwEAAQQFAXABAQEFAwEAEAYJAX8BQYCAwAALB60BCQZtZW1vcnkCAA10MjdfZ2F0ZV9vcGVuAAASdDI3X2Rpc2NvdW50X2hvbGRzAAENdDI3X2xvb2tfYmFjawACDHQyN19ydW5fc2F5cwADEHQyN19jaGVja19wYXNzZXMABBl0MjdfcmVxdWlyZWRfY2hlY2tfcGFzc2VzAAUUdDI3X3JlZF9jaGVja19wYXNzZXMABhZ0MjdfY291bnRzX2FzX3JlcXVpcmVkAAcKmwEIDgAgASACckUgAEEAR3ELDgAgACABQf8BcUEBRnELIQAgAEEBQQJBACACQQJGGyACQQFGGyAAIAFBAXEbIAAbCxQAQQFBAkEAIABBAkYbIABBAUYbCyMAAkAgAkEBcUUNACADQQFxDQAgACABQQFzcSAEcSECCyACCwcAIAAgAXELDQAgAEEBcyABcSACcQsKACAAQQFzIAFyCw=="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function countsAsRequired(rulesetReadable, required) { + return (W.t27_counts_as_required(rulesetReadable ? 1 : 0, required ? 1 : 0) & 1) !== 0; +} +function redCheckPasses(required, concluded, discounted) { + return (W.t27_red_check_passes(required ? 1 : 0, concluded ? 1 : 0, discounted ? 1 : 0) & 1) !== 0; +} +function requiredCheckPasses(posted, green) { + return (W.t27_required_check_passes(posted ? 1 : 0, green ? 1 : 0) & 1) !== 0; +} +function checkPasses(rulesetReadable, required, concluded, green, discounted) { + return (W.t27_check_passes(rulesetReadable ? 1 : 0, required ? 1 : 0, concluded ? 1 : 0, green ? 1 : 0, discounted ? 1 : 0) & 1) !== 0; +} +function runSays(run) { + return W.t27_run_says(int(run, 0, 255, "run_says.run")) & 255; +} +function lookBack(soFar, atOrAfterMergeBase, run) { + return W.t27_look_back(int(soFar, 0, 255, "look_back.so_far"), atOrAfterMergeBase ? 1 : 0, int(run, 0, 255, "look_back.run")) & 255; +} +function discountHolds(beeNamed, master) { + return (W.t27_discount_holds(beeNamed ? 1 : 0, int(master, 0, 255, "discount_holds.master")) & 1) !== 0; +} +function gateOpen(posted, running, blocking) { + return (W.t27_gate_open(int(posted, 0, 4294967295, "gate_open.posted"), int(running, 0, 4294967295, "gate_open.running"), int(blocking, 0, 4294967295, "gate_open.blocking")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + checkPasses, + countsAsRequired, + discountHolds, + gateOpen, + lookBack, + redCheckPasses, + requiredCheckPasses, + runSays +}; diff --git a/apps/website/public/widgets/hive-master-red/logic.t27 b/apps/website/public/widgets/hive-master-red/logic.t27 new file mode 100644 index 0000000000..cb217d2ebb --- /dev/null +++ b/apps/website/public/widgets/hive-master-red/logic.t27 @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/merger_gate.t27 -- when the merger may merge a pull request the reviewer bee approved +; The merger is .github/workflows/auto-merge-ready-prs.yml, step "Find ready PRs" (bash and jq). +; It does not import this spec, and t27c gen-js lowers no function bodies (#7300). +; The two meet in tools/bees/merger_gate_selftest.py: a test below that names a scenario +; asserts the verdict that scenario pins for the workflow. +; Negative controls: discounted required check, unconcluded check, required check never posted, +; unreadable ruleset with a discounted red, zero posted checks, a red discounted while master's run +; of the same check was CANCELLED (#7317) +; ASCII only (L3) +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenMergerGate; + +/// With the ruleset unreadable the merger cannot tell which checks are required, +/// so every check counts as required and no red one is discountable (#5776 item 2). +pub fn counts_as_required(ruleset_readable: bool, required: bool) -> bool { + return !ruleset_readable || required; +} + +/// A red check passes only when it is not required, has concluded, and is discounted +pub fn red_check_passes(required: bool, concluded: bool, discounted: bool) -> bool { + return !required && concluded && discounted; +} + +/// A required check passes when it is posted and green +pub fn required_check_passes(posted: bool, green: bool) -> bool { + return posted && green; +} + +/// One posted check. Not concluded (PENDING, EXPECTED, no state yet) blocks; +/// green (SUCCESS or SKIPPED) passes; a red passes only as a discounted check +/// that does not count as required. +pub fn check_passes(ruleset_readable: bool, required: bool, concluded: bool, green: bool, discounted: bool) -> bool { + if (!concluded) { return false; } + if (green) { return true; } + return red_check_passes(counts_as_required(ruleset_readable, required), true, discounted); +} + +// --------------------------------------------------------------------------- +// What master says about a discounted red (#7317) +// +// The bee's `discounted-check:` line says "red on master too". On 2026-10-06 PR #6956 merged with its +// Corpus ratchet red because master's run of that check at the PR's base was CANCELLED, and a cancelled +// run was read as a red one. A cancelled run says nothing about the code: a superseding push stopped it. +// So the line is necessary and no longer sufficient. The merger reads master itself: the runs of the +// red check's own job on master, newest first, and only a COMPLETED FAILURE at the PR's merge base or +// later makes the red inherited. Everything else is UNKNOWN, and UNKNOWN never discounts. + +/// One master run of the red check, as the merger reads its job's status and conclusion. +/// The codes are mirrored by number in the merger's selftest fixtures; a new one takes the next code. +pub const RUN_MISSING : u8 = 0; +pub const RUN_FAILURE : u8 = 1; +pub const RUN_SUCCESS : u8 = 2; +pub const RUN_CANCELLED : u8 = 3; +pub const RUN_SKIPPED : u8 = 4; +pub const RUN_TIMED_OUT : u8 = 5; +/// queued or in progress +pub const RUN_RUNNING : u8 = 6; +/// neutral, action_required, startup_failure, stale: completed, but neither a pass nor a fail +pub const RUN_OTHER : u8 = 7; + +/// What master says about the PR's red. +pub const MASTER_UNKNOWN : u8 = 0; +/// a completed failure: the red is inherited +pub const MASTER_RED : u8 = 1; +/// a completed success: master was green, the red is the PR's own +pub const MASTER_GREEN : u8 = 2; + +/// One run on its own. Only a completed failure or a completed success decides anything. +pub fn run_says(run: u8) -> u8 { + if (run == RUN_FAILURE) { return MASTER_RED; } + if (run == RUN_SUCCESS) { return MASTER_GREEN; } + return MASTER_UNKNOWN; +} + +/// One step of the look-back over master's runs, newest first, starting from MASTER_UNKNOWN. +/// The newest DECISIVE run wins: a cancelled newest run is looked past, a success is not. +/// A run older than the PR's merge base is not evidence about the code the PR started from. +pub fn look_back(so_far: u8, at_or_after_merge_base: bool, run: u8) -> u8 { + if (so_far != MASTER_UNKNOWN) { return so_far; } + if (!at_or_after_merge_base) { return MASTER_UNKNOWN; } + return run_says(run); +} + +/// A red is discounted only when the bee's approval names it AND master says red. +/// This is the `discounted` that check_passes takes. +pub fn discount_holds(bee_named: bool, master: u8) -> bool { + return bee_named && master == MASTER_RED; +} + +/// The gate opens when at least one check has posted, none is still running, +/// and none blocks. Zero checks is not zero failures. +pub fn gate_open(posted: u32, running: u32, blocking: u32) -> bool { + return posted > 0 && running == 0 && blocking == 0; +} + +// Tests for counts_as_required + +test counts_as_required_follows_the_ruleset_when_readable { + assert counts_as_required(true, true) == true; + assert counts_as_required(true, false) == false; +} + +test counts_as_required_is_every_check_when_unreadable { + assert counts_as_required(false, false) == true; + assert counts_as_required(false, true) == true; +} + +// Tests for red_check_passes + +// Positive case: non-required check that concluded and is discounted +test red_check_passes_valid_discounted_non_required { + assert red_check_passes(false, true, true) == true; +} + +// Negative control: discounted required check (required=true, cannot be discounted) +test red_check_passes_discounted_required_fails { + assert red_check_passes(true, true, true) == false; +} + +// Negative control: unconcluded check (concluded=false, even if discounted) +test red_check_passes_unconcluded_fails { + assert red_check_passes(false, false, true) == false; +} + +// Negative control: non-required check that concluded but not discounted +test red_check_passes_non_discounted_fails { + assert red_check_passes(false, true, false) == false; +} + +test red_check_passes_all_false_fails { + assert red_check_passes(true, false, false) == false; +} + +// Tests for required_check_passes + +test required_check_passes_posted_and_green_passes { + assert required_check_passes(true, true) == true; +} + +// Self-test scenario "required check never posted" +test required_check_passes_not_posted_fails { + assert required_check_passes(false, true) == false; +} + +test required_check_passes_posted_but_not_green_fails { + assert required_check_passes(true, false) == false; +} + +test required_check_passes_neither_posted_nor_green_fails { + assert required_check_passes(false, false) == false; +} + +// Tests for check_passes: (ruleset_readable, required, concluded, green, discounted) + +test check_passes_green_advisory_and_green_required_pass { + assert check_passes(true, false, true, true, false) == true; + assert check_passes(true, true, true, true, false) == true; +} + +// Self-test scenario "red advisory check, discounted in the bee's approval" +test check_passes_red_advisory_discounted_passes { + assert check_passes(true, false, true, false, true) == true; +} + +// Self-test scenario "red advisory check, not discounted" +test check_passes_red_advisory_not_discounted_blocks { + assert check_passes(true, false, true, false, false) == false; +} + +// Self-test scenario "red REQUIRED check, discounted anyway" +test check_passes_red_required_discounted_blocks { + assert check_passes(true, true, true, false, true) == false; +} + +// Self-test scenarios "a check still running" and "pending commit status blocks": +// not concluded blocks whatever else is true, a discount included +test check_passes_running_check_blocks { + assert check_passes(true, false, false, true, true) == false; + assert check_passes(true, false, false, false, true) == false; +} + +// Self-test scenario "ruleset unreadable: a discounted red check still blocks" +test check_passes_unreadable_ruleset_discounted_red_blocks { + assert check_passes(false, false, true, false, true) == false; +} + +// Self-test scenario "ruleset unreadable, every check green" +test check_passes_unreadable_ruleset_green_passes { + assert check_passes(false, false, true, true, false) == true; +} + +// Tests for gate_open: (posted, running, blocking) + +// Self-test scenario "bee approved this head after it arrived, bee labeled": two green checks +test gate_open_posted_checks_all_pass_opens { + assert gate_open(2, 0, 0) == true; + assert gate_open(1, 0, 0) == true; +} + +// Negative control: zero checks is not zero failures +test gate_open_zero_checks_stays_closed { + assert gate_open(0, 0, 0) == false; +} + +// Self-test scenario "a check still running" +test gate_open_running_check_keeps_it_closed { + assert gate_open(3, 1, 0) == false; +} + +// Self-test scenario "two red checks, only one discounted" +test gate_open_one_blocking_check_keeps_it_closed { + assert gate_open(4, 0, 1) == false; +} + +// Tests for run_says, look_back and discount_holds (#7317) + +test only_a_completed_failure_says_red { + assert run_says(RUN_FAILURE) == MASTER_RED; + assert run_says(RUN_SUCCESS) == MASTER_GREEN; +} + +test every_other_run_says_unknown { + assert run_says(RUN_MISSING) == MASTER_UNKNOWN; + assert run_says(RUN_CANCELLED) == MASTER_UNKNOWN; + assert run_says(RUN_SKIPPED) == MASTER_UNKNOWN; + assert run_says(RUN_TIMED_OUT) == MASTER_UNKNOWN; + assert run_says(RUN_RUNNING) == MASTER_UNKNOWN; + assert run_says(RUN_OTHER) == MASTER_UNKNOWN; +} + +// Self-test scenario "master newest cancelled, older failure at or after the merge base" +test look_back_passes_a_cancelled_run_to_an_older_failure { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_FAILURE) == MASTER_RED; +} + +// Self-test scenario "master failure only before the merge base" +test look_back_stops_at_the_merge_base { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), false, RUN_FAILURE) == MASTER_UNKNOWN; + assert look_back(MASTER_UNKNOWN, false, RUN_SUCCESS) == MASTER_UNKNOWN; +} + +// The newest decisive run wins, in both directions +test look_back_keeps_the_newest_decisive_run { + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_SUCCESS), true, RUN_FAILURE) == MASTER_GREEN; + assert look_back(look_back(MASTER_UNKNOWN, true, RUN_FAILURE), true, RUN_SUCCESS) == MASTER_RED; + assert look_back(MASTER_RED, false, RUN_SUCCESS) == MASTER_RED; +} + +test look_back_over_only_unknown_runs_stays_unknown { + assert look_back(look_back(look_back(MASTER_UNKNOWN, true, RUN_CANCELLED), true, RUN_RUNNING), true, RUN_SKIPPED) == MASTER_UNKNOWN; +} + +// Self-test scenario "master failure": discounted +test discount_holds_on_a_master_failure { + assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_FAILURE)) == true; + assert check_passes(true, false, true, false, discount_holds(true, MASTER_RED)) == true; +} + +// NEGATIVE CONTROL, the #6956 incident: PR red while master's run is CANCELLED -> blocked +test pr_red_while_master_cancelled_is_blocked { + assert discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED)) == false; + assert check_passes(true, false, true, false, discount_holds(true, look_back(MASTER_UNKNOWN, true, RUN_CANCELLED))) == false; +} + +// Self-test scenario "master success": the red is the PR's own -> blocked +test pr_red_while_master_green_is_blocked { + assert discount_holds(true, MASTER_GREEN) == false; + assert check_passes(true, false, true, false, discount_holds(true, MASTER_GREEN)) == false; +} + +// Master red, but the bee never named the check: blocked +test master_red_without_the_bees_line_is_blocked { + assert discount_holds(false, MASTER_RED) == false; + assert discount_holds(false, MASTER_UNKNOWN) == false; +} + +invariant the_run_and_master_codes_never_move { + assert RUN_MISSING == 0; + assert RUN_FAILURE == 1; + assert RUN_SUCCESS == 2; + assert RUN_CANCELLED == 3; + assert RUN_SKIPPED == 4; + assert RUN_TIMED_OUT == 5; + assert RUN_RUNNING == 6; + assert RUN_OTHER == 7; + assert MASTER_UNKNOWN == 0; + assert MASTER_RED == 1; + assert MASTER_GREEN == 2; +} + +invariant unknown_never_discounts { + assert discount_holds(true, MASTER_UNKNOWN) == false; + assert discount_holds(true, run_says(RUN_MISSING)) == false; + assert discount_holds(true, run_says(RUN_CANCELLED)) == false; + assert discount_holds(true, run_says(RUN_SKIPPED)) == false; + assert discount_holds(true, run_says(RUN_TIMED_OUT)) == false; + assert discount_holds(true, run_says(RUN_RUNNING)) == false; + assert discount_holds(true, run_says(RUN_OTHER)) == false; + assert discount_holds(true, look_back(MASTER_UNKNOWN, false, RUN_FAILURE)) == false; +} diff --git a/apps/website/public/widgets/hive-master-red/tool.js b/apps/website/public/widgets/hive-master-red/tool.js new file mode 100644 index 0000000000..5e0ff0fc95 --- /dev/null +++ b/apps/website/public/widgets/hive-master-red/tool.js @@ -0,0 +1,37 @@ +// hive-master-red/tool.js -- lesson 15 of queen-and-hive: what master's runs say about a red check. +// Each rung is run_says of its run and the running look_back, and the gate is discount_holds, fn +// bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm (./logic.js). Words are SAY_ +// and numbers K_ constants of specs/widgets/hive-master-red.t27. +import { discountHolds, lookBack, runSays } from './logic.js' +import { K, W, ask, controls, el, fill, gate, head, pill, root, say, source, taps } from '../hive-kit.js' + +const N = K('K_RUNS', 4), KINDS = K('K_RUN_KINDS', 8) +const keys = [...Array(N)].map((_, i) => `r${i}`) +const ctl = controls([ + ...keys.map((key) => ({ key, kind: 'hidden', min: 0, max: KINDS - 1 })), + { key: 'base', kind: 'hidden', min: 0, max: 15 }, + { key: 'named', label: say('SAY_NAMED'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { ...Object.fromEntries(keys.map((k, i) => [k, K(`K_PRESET_R${i}`, [])])), base: K('K_PRESET_BASE', []), named: K('K_PRESET_NAMED', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const MASTER_TONES = [8, 6, 0] + +function draw(s) { + let soFar = K('K_MASTER_UNKNOWN', 0) + const rows = keys.map((k, i) => { + const after = !!(s.base & (1 << i)) + soFar = lookBack(soFar, after, s[k]) + const says = runSays(s[k]) + return el('div', { class: 'hk-path' }, + el('p', { class: 'hk-path-head' }, el('code', {}, fill(say('SAY_RUN'), i + 1)), pill(`${say('SAY_SO_FAR')}: ${say('SAY_MASTER_NAMES', soFar)}`, MASTER_TONES[soFar])), + taps([ + { label: say('SAY_RUN_NAMES', s[k]), sub: `${say('SAY_SAYS')} ${say('SAY_MASTER_NAMES', says)}`, tone: MASTER_TONES[says] }, + { label: after ? say('SAY_AFTER') : say('SAY_BEFORE'), pressed: after, tone: after ? 2 : 8 }, + ], (j) => (j === 0 ? ctl.set(k, (s[k] + 1) % KINDS) : ctl.set('base', s.base ^ (1 << i))), fill(say('SAY_RUN'), i + 1))) + }) + const holds = discountHolds(!!s.named, soFar) + out.replaceChildren(head(say('SAY_TAP')), ...rows, gate(holds, holds ? say('SAY_HOLDS') : say('SAY_FAILS'), say('SAY_MASTER_NAMES', soFar))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-next-key/card.png b/apps/website/public/widgets/hive-next-key/card.png new file mode 100644 index 0000000000..9a773fe5cd Binary files /dev/null and b/apps/website/public/widgets/hive-next-key/card.png differ diff --git a/apps/website/public/widgets/hive-next-key/hive-next-key.t27 b/apps/website/public/widgets/hive-next-key/hive-next-key.t27 new file mode 100644 index 0000000000..68f702abc5 --- /dev/null +++ b/apps/website/public/widgets/hive-next-key/hive-next-key.t27 @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-next-key.t27 -- The next key: a bee moves on instead of dying +; Source of truth for public/widgets/hive-next-key/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-next-key/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 18 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the pool size, taps keys between free, tried and resting, and sets +; the attempts and how many keys answered 1305. The ring is next_free(tried, resting, size, last); +; the verdict is step(transient, attempts, that key, 1305 keys, size, the next pool) with +; pool_overloaded beside it. + +; WHAT IT DOES NOT CLAIM: it holds no key: the pool is a picture of masks, bit i is key i + 1. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_next_key; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-next-key"; +pub const TITLE : str = "The next key: a bee moves on instead of dying"; +pub const DESCRIPTION : str = "A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up."; +pub const IMAGE_ALT : str = "Next key card: a pool of 10 keys, key 9 just failed with a 1305 and is tried, keys 10 and 1 rest in red, the others are green. Key 2 has a white ring: next_free picked it. The verdict reads NEXT KEY at attempt 2 of 8."; +pub const HOOK : str = "Measured 2026-10-17: a bee died 7 s into its turn on key 9 of 10 while 33 keys stood by. Now it tries the next free key, never the one that failed."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is next_free, pool_overloaded and step, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 18 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_SIZE_MAX : u8 = 12; +pub const K_KE_TRANSIENT : u8 = 1; +pub const K_ATTEMPTS_MAX : u8 = 9; +pub const K_OVERLOAD_MAX : u8 = 5; +pub const K_NO_KEY : u32 = 255; +pub const K_POOL_OVERLOAD_KEYS : u32 = 3; +pub const K_MAX_ATTEMPTS_PER_TURN : u32 = 8; +pub const K_PRESET_SIZE : [4]u8 = [10, 10, 10, 4]; +pub const K_PRESET_LAST : [4]u8 = [8, 8, 3, 0]; +pub const K_PRESET_TRIED : [4]u16 = [256, 256, 1023, 1]; +pub const K_PRESET_RESTING : [4]u16 = [513, 513, 0, 14]; +pub const K_PRESET_ATTEMPTS : [4]u8 = [2, 4, 8, 2]; +pub const K_PRESET_OVERLOAD : [4]u8 = [1, 3, 1, 1]; +pub const K_START_PRESET : u8 = 0; +; The pool list the bee falls back on: two pools, this one (pool 0) tried, none resting, this one +; failed last. next_free of it names the next pool. +pub const K_POOL_LIST_TRIED : u8 = 1; +pub const K_POOL_LIST_RESTING : u8 = 0; +pub const K_POOL_LIST_SIZE : u8 = 2; +pub const K_POOL_LIST_LAST : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Key 9 of 10 failed", "Three keys said 1305", "Every key tried", "A small pool"]; +pub const SAY_ASK : str = "A bee's key was refused mid-task. Which key does it try next?"; +pub const SAY_SIZE : str = "keys in the pool"; +pub const SAY_ATTEMPTS : str = "attempts this turn"; +pub const SAY_OVERLOAD : str = "keys that answered 1305 in the last minute"; +pub const SAY_KEY_STATES : [3]str = ["free", "tried", "resting"]; +pub const SAY_TAP : str = "tap a key: free, tried, resting"; +pub const SAY_LAST : str = "failed last"; +pub const SAY_STEP_NAMES : [5]str = ["DONE", "NEXT KEY", "NEXT POOL", "GIVE UP", "FAIL"]; +pub const SAY_NEXT : str = "next key"; +pub const SAY_NONE : str = "none left"; +pub const SAY_POOL_OUT : str = "pool overloaded"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_NOTE : str = "1305 is an overload of the pool's model, not of one key: once 3 distinct keys said it within a minute, the bee moves to the next pool instead of burning the rest. At 8 attempts the turn ends, and its claim is released."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_bounds_are_the_specs { + assert K_NO_KEY == 255; + assert K_POOL_OVERLOAD_KEYS == 3; + assert K_MAX_ATTEMPTS_PER_TURN == 8; +} + +test the_pool_list_has_this_pool_tried { + assert K_POOL_LIST_SIZE == 2; + assert K_POOL_LIST_TRIED == 1; + assert K_POOL_LIST_LAST < K_POOL_LIST_SIZE; +} + +test the_start_pool_is_ten_keys { + assert K_PRESET_SIZE[0] == 10; + assert K_PRESET_LAST[0] < K_PRESET_SIZE[0]; + assert K_PRESET_TRIED[0] == 256; +} diff --git a/apps/website/public/widgets/hive-next-key/index.html b/apps/website/public/widgets/hive-next-key/index.html new file mode 100644 index 0000000000..786dc10fbf --- /dev/null +++ b/apps/website/public/widgets/hive-next-key/index.html @@ -0,0 +1,47 @@ + + + + + + +The next key: a bee moves on instead of dying · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The next key: a bee moves on instead of dying

+

A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is next_free, pool_overloaded and step, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 18 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-next-key/logic.js b/apps/website/public/widgets/hive-next-key/logic.js new file mode 100644 index 0000000000..4fdf6c30e0 --- /dev/null +++ b/apps/website/public/widgets/hive-next-key/logic.js @@ -0,0 +1,60 @@ +// Generated by scripts/t27-logic.mjs from key_failover.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 822367357fc6b487c1c7fb7ac8369165e6a7e6636fd9b0dcafec29e9a6274301 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-next-key/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABHgRgAX8Bf2AGf39/f39/AX9gAn9/AX9gBH9/f38BfwMLCgABAgMCAAACAgAEBQFwAQEBBQMBABAGCQF/AUGAgMAACwezAQsGbWVtb3J5AgARdDI3X2NoYXJnZXNfaXNzdWUAAAh0Mjdfc3RlcAABE3QyN19wb29sX292ZXJsb2FkZWQAAg10MjdfbmV4dF9mcmVlAAMKdDI3X2lzX3NldAAEB3QyN19iaXQABQ50Mjdfa2V5X2Nsb3NlcwAGFHQyN19jb29sZG93bl9zZWNvbmRzAAcOdDI3X2Vycm9yX2tpbmQACBF0MjdfaXNfcXVvdGFfY29kZQAJCqsECgcAIABBBEYLagEBfwJAIAANAEEADwtBAyEGAkAgAEEDTQ0AQQQPCwJAIAFBB0sNAEEBIQZBASEBAkAgAEEBRw0AIANBA0kgBEF/aiADT3EhAQsCQCABRQ0AIAJB/wFHDQELQQNBAiAFQf8BRhshBgsgBgsQACAAQQJLIAFBf2ogAElyC2IBAX8CQCACQSBLDQAgA0EBakEAIAMgAkkiAxshBCABIAByIQAgAiADayEDA0AgA0UNASAEIAJwIQEgA0F/aiEDIARBAWohBEEAQQEgAXQgAUEfSxsgAHENAAsgAQ8LQf8BCxUAQQBBASABdCABQR9LGyAAcUEARwsPAEEAQQEgAHQgAEEfSxsLCwAgAEH+AXFBAkYLUwEBf0GIDiECAkACQAJAIABBf2oOAwACAgELQQ8hAkEAIQADQCABIABGDQICQCAAQQRHDQBB8AEPCyAAQQFqIQAgAkEBdCECDAALC0EAIQILIAILkwEBAX9BAiECAkAgAUHZCEYNAAJAIAFBoApGDQAgAUHkdWpBDkkNAUEBIQIgAUHqdWoOBAEAAAEAC0EBIQICQAJAAkACQAJAIABB73xqDggDAgMBAQEBBQALIABFDQQgAEGtA0YNBAsgAEHXBE0NAkEEDwtBAg8LQQMPCyAAQfMDSw0AQQRBACAAQY8DSxshAgsgAgsnAAJAIABB2QhHDQBBAQ8LAkAgAEGgCkcNAEEADwsgAEHkdWpBDkkL"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function isQuotaCode(code) { + return (W.t27_is_quota_code(int(code, 0, 4294967295, "is_quota_code.code")) & 1) !== 0; +} +function errorKind(status, zaiCode) { + return W.t27_error_kind(int(status, 0, 4294967295, "error_kind.status"), int(zaiCode, 0, 4294967295, "error_kind.zai_code")) & 255; +} +function cooldownSeconds(kind, strikes) { + return W.t27_cooldown_seconds(int(kind, 0, 255, "cooldown_seconds.kind"), int(strikes, 0, 4294967295, "cooldown_seconds.strikes")) >>> 0; +} +function keyCloses(kind) { + return (W.t27_key_closes(int(kind, 0, 255, "key_closes.kind")) & 1) !== 0; +} +function bit(i) { + return W.t27_bit(int(i, 0, 4294967295, "bit.i")) >>> 0; +} +function isSet(mask, i) { + return (W.t27_is_set(int(mask, 0, 4294967295, "is_set.mask"), int(i, 0, 4294967295, "is_set.i")) & 1) !== 0; +} +function nextFree(tried, blocked, size, last) { + return W.t27_next_free(int(tried, 0, 4294967295, "next_free.tried"), int(blocked, 0, 4294967295, "next_free.blocked"), int(size, 0, 4294967295, "next_free.size"), int(last, 0, 4294967295, "next_free.last")) >>> 0; +} +function poolOverloaded(distinctKeys1305, poolSize) { + return (W.t27_pool_overloaded(int(distinctKeys1305, 0, 4294967295, "pool_overloaded.distinct_keys_1305"), int(poolSize, 0, 4294967295, "pool_overloaded.pool_size")) & 1) !== 0; +} +function step(kind, attempts, nextKey, distinctKeys1305, poolSize, nextPool) { + return W.t27_step(int(kind, 0, 255, "step.kind"), int(attempts, 0, 4294967295, "step.attempts"), int(nextKey, 0, 4294967295, "step.next_key"), int(distinctKeys1305, 0, 4294967295, "step.distinct_keys_1305"), int(poolSize, 0, 4294967295, "step.pool_size"), int(nextPool, 0, 4294967295, "step.next_pool")) & 255; +} +function chargesIssue(kind) { + return (W.t27_charges_issue(int(kind, 0, 255, "charges_issue.kind")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + bit, + chargesIssue, + cooldownSeconds, + errorKind, + isQuotaCode, + isSet, + keyCloses, + nextFree, + poolOverloaded, + step +}; diff --git a/apps/website/public/widgets/hive-next-key/logic.t27 b/apps/website/public/widgets/hive-next-key/logic.t27 new file mode 100644 index 0000000000..3ce96bef2b --- /dev/null +++ b/apps/website/public/widgets/hive-next-key/logic.t27 @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/key_failover.t27 -- a bee whose key is refused moves to another key, then another pool (gHashTag/t27#9993). +; Measured 2026-10-17 13:00-15:18Z in the Railway logs of trios-bee-runner-2: 93 bee turns, 17 of them +; ended after 0 steps; at least 22 AI_RetryError on z.ai "429 code 1305" (temporary overload), in bursts +; at 13:53, 13:59, 14:08 (16 in one minute), 14:19, 14:47 and 14:50. trios-bee-runner-1: about 6 of 48 +; turns ended after 0 steps. The bee for #9967 started at 14:50:47 on pool-2 key 9/10 and died at 14:50:54. +; The cause: a bee gets ONE key when it is dispatched (trios queen-dispatch.ts resolveWorkerProvider, +; round-robin) and keeps it. On 1305 the SDK retried the same key three times, the turn closed with no +; completion frame, and the dispatch ended "cause undetermined" while 33 other keys in two pools (24 and +; 10) stood by. The reviewer already does the right thing (trios queen-tick.ts, "a transient refusal +; tries the next lane too", after it lost 16 of 16 calls to 1302/1305 on 2026-09-23); bees did not. +; The rule here, after Envoy's retry_host_predicate previous_hosts plus outlier ejection, and the LiteLLM +; router's cooldown-then-fallback: +; 1. classify the refusal: transient, quota, auth or a bad request (error_kind); +; 2. a transient refusal rests the key for seconds (cooldown_seconds), a quota or auth refusal closes +; it for the long break -- 1302 and 1305 never close a key and never retire work; +; 3. the next attempt goes to the next free key of the pool, never the key that just failed +; (next_free); 1305 is an overload of the pool's MODEL, not of one key, so once POOL_OVERLOAD_KEYS +; distinct keys of one pool answered 1305 inside OVERLOAD_WINDOW_SECONDS the bee moves to the next +; pool instead of burning the rest (pool_overloaded); +; 4. at most MAX_ATTEMPTS_PER_TURN attempts, then the turn ends as provider_overloaded (trios#1708), +; its claim is released (trios#1886), and the end is not charged to the issue (charges_issue). +; Masks: bit i of a u32 mask is key i of the pool (or pool i of the pool list); a pool holds at most +; POOL_MAX keys. NO_KEY means "none left". The host keeps the clock and the masks; this file decides. +; Not here: the wiring into trios. It lands as generated code, not hand-written TypeScript. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenKeyFailover; + +; --- 1. What the provider said ----------------------------------------------------------------------- + +/// KE_OK: the request was answered. KE_TRANSIENT: refused for now, another key or a moment later may +/// pass. KE_QUOTA: the key's quota, plan or balance is spent. KE_AUTH: the key itself is refused. +/// KE_REQUEST: the request is wrong; every key would answer the same. +pub const KE_OK : u8 = 0; +pub const KE_TRANSIENT : u8 = 1; +pub const KE_QUOTA : u8 = 2; +pub const KE_AUTH : u8 = 3; +pub const KE_REQUEST : u8 = 4; + +/// The z.ai rate limit and temporary overload codes. Both arrive as HTTP 429 and both clear on their own. +pub const ZAI_RATE_LIMIT : u32 = 1302; +pub const ZAI_OVERLOAD : u32 = 1305; + +/// The z.ai codes whose documented meaning is a spent quota, plan or balance: 1113 and 1308-1321 +/// except 1312, the closed list ZAI_QUOTA_EXHAUSTED_CODES in trios queen-dispatch.ts (#1301). +pub fn is_quota_code(code: u32) -> bool { + if (code == 1113) { return true; } + if (code == 1312) { return false; } + if (code < 1308) { return false; } + return code <= 1321; +} + +/// `status` is the HTTP status, 0 when no response came at all (timeout, reset, abort). `zai_code` is +/// the business code in the body, 0 when there is none. The code wins over the status, because z.ai +/// sends a spent quota and a passing overload with the same 429. +pub fn error_kind(status: u32, zai_code: u32) -> u8 { + if (is_quota_code(zai_code)) { return KE_QUOTA; } + if (zai_code == ZAI_RATE_LIMIT) { return KE_TRANSIENT; } + if (zai_code == ZAI_OVERLOAD) { return KE_TRANSIENT; } + if (status == 0) { return KE_TRANSIENT; } + if (status == 402) { return KE_QUOTA; } + if (status == 401) { return KE_AUTH; } + if (status == 403) { return KE_AUTH; } + if (status == 408) { return KE_TRANSIENT; } + if (status == 429) { return KE_TRANSIENT; } + if (status >= 600) { return KE_REQUEST; } + if (status >= 500) { return KE_TRANSIENT; } + if (status >= 400) { return KE_REQUEST; } + return KE_OK; +} + +; --- 2. How long a refused key rests ------------------------------------------------------------------ + +/// A transient refusal rests the key COOL_BASE_SECONDS, doubled per strike in a row, never more than +/// COOL_CAP_SECONDS: long enough that the next bee does not hit the same overloaded key at once, short +/// enough that 34 keys are never all resting through a one-minute burst like 14:08. +pub const COOL_BASE_SECONDS : u32 = 15; +pub const COOL_CAP_SECONDS : u32 = 240; +/// A spent quota or a refused key closes for the long break the runtime already uses (30 minutes). +pub const KEY_BREAK_SECONDS : u32 = 1800; + +/// `strikes` is the key's transient refusals in a row before this one. The cap is the base doubled +/// exactly four times (an invariant pins it), so the doubling meets the cap and never steps past it. +pub fn cooldown_seconds(kind: u8, strikes: u32) -> u32 { + if (kind == KE_QUOTA) { return KEY_BREAK_SECONDS; } + if (kind == KE_AUTH) { return KEY_BREAK_SECONDS; } + if (kind != KE_TRANSIENT) { return 0; } + var wait : u32 = COOL_BASE_SECONDS; + var n : u32 = 0; + while (n < strikes) { + if (wait >= COOL_CAP_SECONDS) { return COOL_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + return wait; +} + +/// Whether the refusal closes the key for the long break. A transient refusal never does. +pub fn key_closes(kind: u8) -> bool { + if (kind == KE_QUOTA) { return true; } + return kind == KE_AUTH; +} + +; --- 3. The next key, the next pool ------------------------------------------------------------------ + +pub const POOL_MAX : u32 = 32; +pub const NO_KEY : u32 = 255; + +pub fn bit(i: u32) -> u32 { + if (i >= POOL_MAX) { return 0; } + return 1 << i; +} + +pub fn is_set(mask: u32, i: u32) -> bool { + return (mask & bit(i)) != 0; +} + +/// The next index after `last`, wrapping, that is neither tried in this turn nor blocked (resting or +/// closed), and is never `last` itself: the scan visits the size - 1 other indices only. `last` out of +/// range (NO_KEY) visits all of them from index 0. NO_KEY when none is free. A list longer than +/// POOL_MAX does not fit a mask and is refused (NO_KEY), never read as its first 32. The same rule +/// picks a key inside a pool and a pool inside the pool list. +pub fn next_free(tried: u32, blocked: u32, size: u32, last: u32) -> u32 { + if (size > POOL_MAX) { return NO_KEY; } + var start : u32 = 0; + var count : u32 = size; + if (last < size) { + start = last + 1; + count = size - 1; + } + var k : u32 = 0; + while (k < count) { + var i : u32 = (start + k) % size; + if (is_set(tried | blocked, i) == false) { return i; } + k = k + 1; + } + return NO_KEY; +} + +/// 1305 from POOL_OVERLOAD_KEYS distinct keys of one pool inside OVERLOAD_WINDOW_SECONDS is the +/// pool's model overloaded. A pool smaller than that is overloaded when every key said it. +pub const POOL_OVERLOAD_KEYS : u32 = 3; +pub const OVERLOAD_WINDOW_SECONDS : u32 = 60; + +pub fn pool_overloaded(distinct_keys_1305: u32, pool_size: u32) -> bool { + if (distinct_keys_1305 >= POOL_OVERLOAD_KEYS) { return true; } + if (pool_size == 0) { return false; } + return distinct_keys_1305 >= pool_size; +} + +; --- 4. The step after a refusal --------------------------------------------------------------------- + +/// FA_DONE: answered, go on. FA_NEXT_KEY: retry on next_free's key. FA_NEXT_POOL: retry on the next +/// pool. FA_GIVE_UP: no key left; the turn ends as provider_overloaded. FA_FAIL: the request is wrong. +pub const FA_DONE : u8 = 0; +pub const FA_NEXT_KEY : u8 = 1; +pub const FA_NEXT_POOL : u8 = 2; +pub const FA_GIVE_UP : u8 = 3; +pub const FA_FAIL : u8 = 4; + +/// The bound on attempts in one turn, the failed one included: three keys of the first pool to see an +/// overload, three of the second, and two to spare. It keeps a turn from spinning through 34 keys. +pub const MAX_ATTEMPTS_PER_TURN : u32 = 8; + +/// `attempts` counts the attempts of this turn so far, this one included. `next_key` and `next_pool` +/// are next_free's answers for the current pool and for the pool list (NO_KEY when none). +pub fn step(kind: u8, attempts: u32, next_key: u32, distinct_keys_1305: u32, pool_size: u32, next_pool: u32) -> u8 { + if (kind == KE_OK) { return FA_DONE; } + if (kind >= KE_REQUEST) { return FA_FAIL; } + if (attempts >= MAX_ATTEMPTS_PER_TURN) { return FA_GIVE_UP; } + var pool_out : bool = false; + if (kind == KE_TRANSIENT) { pool_out = pool_overloaded(distinct_keys_1305, pool_size); } + if (pool_out == false) { + if (next_key != NO_KEY) { return FA_NEXT_KEY; } + } + if (next_pool != NO_KEY) { return FA_NEXT_POOL; } + return FA_GIVE_UP; +} + +/// Whether a turn that ended on this kind counts against the issue. A provider that is overloaded, +/// spent or refusing a key says nothing about the work, so it never retires the issue; a wrong request +/// does. Only KE_REQUEST charges. This narrows dispatch_exit.t27 counts_against_issue, which charges +/// every DX_PROVIDER_ERROR. +pub fn charges_issue(kind: u8) -> bool { + return kind == KE_REQUEST; +} + +; --- 5. Tests ---------------------------------------------------------------------------------------- + +test the_quota_list_is_the_closed_list_of_the_runtime { + assert is_quota_code(1113) == true; + assert is_quota_code(1308) == true; + assert is_quota_code(1311) == true; + assert is_quota_code(1312) == false; + assert is_quota_code(1313) == true; + assert is_quota_code(1321) == true; + assert is_quota_code(1322) == false; + assert is_quota_code(1307) == false; + assert is_quota_code(1112) == false; + assert is_quota_code(1114) == false; + assert is_quota_code(ZAI_RATE_LIMIT) == false; + assert is_quota_code(ZAI_OVERLOAD) == false; + assert is_quota_code(0) == false; +} + +test the_code_wins_over_the_status { + assert error_kind(429, 1305) == KE_TRANSIENT; + assert error_kind(429, 1302) == KE_TRANSIENT; + assert error_kind(429, 1113) == KE_QUOTA; + assert error_kind(429, 1316) == KE_QUOTA; + assert error_kind(200, 1308) == KE_QUOTA; + assert error_kind(400, 1305) == KE_TRANSIENT; + assert error_kind(400, 1302) == KE_TRANSIENT; + assert error_kind(429, 1312) == KE_TRANSIENT; +} + +test a_status_without_a_code_falls_in_its_kind { + assert error_kind(0, 0) == KE_TRANSIENT; + assert error_kind(200, 0) == KE_OK; + assert error_kind(399, 0) == KE_OK; + assert error_kind(400, 0) == KE_REQUEST; + assert error_kind(401, 0) == KE_AUTH; + assert error_kind(402, 0) == KE_QUOTA; + assert error_kind(403, 0) == KE_AUTH; + assert error_kind(404, 0) == KE_REQUEST; + assert error_kind(407, 0) == KE_REQUEST; + assert error_kind(408, 0) == KE_TRANSIENT; + assert error_kind(409, 0) == KE_REQUEST; + assert error_kind(428, 0) == KE_REQUEST; + assert error_kind(429, 0) == KE_TRANSIENT; + assert error_kind(430, 0) == KE_REQUEST; + assert error_kind(499, 0) == KE_REQUEST; + assert error_kind(500, 0) == KE_TRANSIENT; + assert error_kind(503, 0) == KE_TRANSIENT; + assert error_kind(529, 0) == KE_TRANSIENT; + assert error_kind(599, 0) == KE_TRANSIENT; + assert error_kind(600, 0) == KE_REQUEST; +} + +test a_transient_refusal_rests_seconds_and_a_spent_key_closes { + assert cooldown_seconds(KE_TRANSIENT, 0) == 15; + assert cooldown_seconds(KE_TRANSIENT, 1) == 30; + assert cooldown_seconds(KE_TRANSIENT, 2) == 60; + assert cooldown_seconds(KE_TRANSIENT, 3) == 120; + assert cooldown_seconds(KE_TRANSIENT, 4) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 5) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_TRANSIENT, 4000000000) == COOL_CAP_SECONDS; + assert cooldown_seconds(KE_QUOTA, 0) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_AUTH, 7) == KEY_BREAK_SECONDS; + assert cooldown_seconds(KE_OK, 3) == 0; + assert cooldown_seconds(KE_REQUEST, 3) == 0; + assert key_closes(KE_QUOTA) == true; + assert key_closes(KE_AUTH) == true; + assert key_closes(KE_TRANSIENT) == false; + assert key_closes(KE_OK) == false; + assert key_closes(KE_REQUEST) == false; +} + +test bits_name_keys { + assert bit(0) == 1; + assert bit(3) == 8; + assert bit(31) == 2147483648; + assert bit(32) == 0; + assert is_set(8, 3) == true; + assert is_set(8, 2) == false; + assert is_set(4294967295, 31) == true; + assert is_set(4294967295, 32) == false; +} + +test the_next_key_is_never_the_one_that_failed { + // the #9967 case: pool 2 has 10 keys, key 9 answered 1305; the next try wraps to key 0 + assert next_free(bit(9), 0, 10, 9) == 0; + // even when the failed key is not marked tried, it is skipped + assert next_free(0, 0, 10, 9) == 0; + assert next_free(0, 0, 10, 4) == 5; + // a pool of one key has no other key + assert next_free(0, 0, 1, 0) == NO_KEY; + // tried and resting keys are both skipped + assert next_free(bit(5), bit(6), 10, 4) == 7; + assert next_free(bit(0) | bit(1), 0, 3, 2) == NO_KEY; + // a fresh turn starts at key 0 + assert next_free(0, 0, 24, NO_KEY) == 0; + assert next_free(bit(0), 0, 24, NO_KEY) == 1; + // every key but the failed one is taken + assert next_free(4294967295 - bit(9), 0, 10, 9) == NO_KEY; + // an empty pool has nothing + assert next_free(0, 0, 0, NO_KEY) == NO_KEY; + // `last` just past the end is out of range: the scan starts at 0 + assert next_free(0, 0, 10, 10) == 0; + // a full mask of POOL_MAX keys wraps from the top index + assert next_free(0, 0, 32, 31) == 0; + assert next_free(0, 0, 32, NO_KEY) == 0; + assert next_free(0, 4294967295 - bit(31), 32, 30) == 31; + // a list longer than POOL_MAX does not fit a mask and is refused + assert next_free(0, 0, 33, NO_KEY) == NO_KEY; + assert next_free(0, 0, 40, 31) == NO_KEY; +} + +test an_overload_seen_on_three_keys_is_the_pools_model { + assert pool_overloaded(0, 24) == false; + assert pool_overloaded(2, 24) == false; + assert pool_overloaded(3, 24) == true; + assert pool_overloaded(16, 24) == true; + assert pool_overloaded(2, 2) == true; + assert pool_overloaded(1, 2) == false; + assert pool_overloaded(1, 1) == true; + assert pool_overloaded(0, 0) == false; +} + +/// The conformance table: one assert per row. Columns: kind, attempts, next key, distinct keys that +/// answered 1305 in the window, pool size, next pool. +test conformance_rows { + // r01 answered: go on + assert step(KE_OK, 1, 5, 0, 24, 1) == FA_DONE; + // r02 a wrong request fails, whatever keys are left + assert step(KE_REQUEST, 1, 5, 0, 24, 1) == FA_FAIL; + // r03 an unknown kind is read as a wrong request + assert step(9, 1, 5, 0, 24, 1) == FA_FAIL; + // r04 the #9967 case: first 1305 on pool 2, keys left: next key, not the same key + assert step(KE_TRANSIENT, 1, 0, 1, 10, 0) == FA_NEXT_KEY; + // r05 a rate limit (1302) on a fresh pool: next key + assert step(KE_TRANSIENT, 1, 3, 0, 24, 1) == FA_NEXT_KEY; + // r06 the third distinct key said 1305: the model is overloaded, next pool + assert step(KE_TRANSIENT, 3, 4, 3, 24, 1) == FA_NEXT_POOL; + // r07 the 14:08 burst: overloaded and no other pool: give up, do not burn the rest + assert step(KE_TRANSIENT, 3, 4, 3, 24, NO_KEY) == FA_GIVE_UP; + // r08 a spent quota on one key does not mean an overloaded pool: next key + assert step(KE_QUOTA, 1, 2, 5, 24, 1) == FA_NEXT_KEY; + // r09 a refused key: next key + assert step(KE_AUTH, 1, 2, 0, 24, 1) == FA_NEXT_KEY; + // r10 no key left in the pool: next pool + assert step(KE_TRANSIENT, 2, NO_KEY, 1, 24, 1) == FA_NEXT_POOL; + // r11 no key and no pool left: give up + assert step(KE_QUOTA, 2, NO_KEY, 0, 24, NO_KEY) == FA_GIVE_UP; + // r12 the attempt bound stops a turn with keys still left + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_GIVE_UP; + // r13 one attempt below the bound still moves on + assert step(KE_TRANSIENT, MAX_ATTEMPTS_PER_TURN - 1, 4, 0, 24, 1) == FA_NEXT_KEY; + // r14 a two-key pool where both said 1305 is overloaded + assert step(KE_TRANSIENT, 2, NO_KEY, 2, 2, 0) == FA_NEXT_POOL; + // r15 a 503 with no 1305 seen keeps to the pool + assert step(KE_TRANSIENT, 2, 7, 0, 24, 1) == FA_NEXT_KEY; + // r16 the bound beats a wrong request? no: a wrong request fails first + assert step(KE_REQUEST, MAX_ATTEMPTS_PER_TURN, 4, 0, 24, 1) == FA_FAIL; + // r17 an answer after many attempts is still an answer + assert step(KE_OK, MAX_ATTEMPTS_PER_TURN, NO_KEY, 9, 24, NO_KEY) == FA_DONE; +} + +test an_overloaded_provider_never_retires_the_issue { + assert charges_issue(KE_TRANSIENT) == false; + assert charges_issue(KE_QUOTA) == false; + assert charges_issue(KE_AUTH) == false; + assert charges_issue(KE_REQUEST) == true; + assert charges_issue(KE_OK) == false; + assert charges_issue(9) == false; +} + +; --- 6. Invariants ----------------------------------------------------------------------------------- + +invariant the_kinds_and_steps_are_distinct { + assert KE_OK == 0 and KE_TRANSIENT == 1 and KE_QUOTA == 2 and KE_AUTH == 3 and KE_REQUEST == 4; + assert FA_DONE == 0 and FA_NEXT_KEY == 1 and FA_NEXT_POOL == 2 and FA_GIVE_UP == 3 and FA_FAIL == 4; +} + +/// A transient rest is always shorter than the long break, so a 1305 can never close a key. +invariant a_rest_is_shorter_than_a_break { + assert COOL_BASE_SECONDS > 0 and COOL_CAP_SECONDS == COOL_BASE_SECONDS * 16; + assert COOL_CAP_SECONDS < KEY_BREAK_SECONDS and KEY_BREAK_SECONDS == 1800; +} + +/// The bound leaves room to see an overload in one pool and still try the next: the overload is +/// detected before the bound stops the turn. +invariant the_bound_leaves_room_for_a_second_pool { + assert MAX_ATTEMPTS_PER_TURN > POOL_OVERLOAD_KEYS * 2; + assert POOL_OVERLOAD_KEYS >= 2 and OVERLOAD_WINDOW_SECONDS > 0; +} + +/// NO_KEY is no index a mask can hold. +invariant no_key_is_out_of_range { + assert POOL_MAX == 32 and NO_KEY >= POOL_MAX; + assert ZAI_RATE_LIMIT == 1302 and ZAI_OVERLOAD == 1305; +} diff --git a/apps/website/public/widgets/hive-next-key/tool.js b/apps/website/public/widgets/hive-next-key/tool.js new file mode 100644 index 0000000000..cedbc87f25 --- /dev/null +++ b/apps/website/public/widgets/hive-next-key/tool.js @@ -0,0 +1,49 @@ +// hive-next-key/tool.js -- lesson 18 of queen-and-hive: which key a bee tries next. The ring is +// next_free, the verdict step and the tile pool_overloaded, fn bodies of gHashTag/t27 +// specs/queen/key_failover.t27 compiled to wasm (./logic.js). The pool is drawn from two masks, bit i +// for key i + 1; a second pool is always there, as next_free of the pool list. Words are SAY_ and +// numbers K_ constants of specs/widgets/hive-next-key.t27. +import { nextFree, poolOverloaded, step } from './logic.js' +import { K, W, ask, comb, controls, el, head, root, say, source, tiles, verdict } from '../hive-kit.js' + +const TRANSIENT = K('K_KE_TRANSIENT', 1), NO_KEY = K('K_NO_KEY', 255), SMAX = K('K_SIZE_MAX', 12) +const ctl = controls([ + { key: 'size', label: say('SAY_SIZE'), min: 1, max: SMAX, step: 1 }, + { key: 'attempts', label: say('SAY_ATTEMPTS'), min: 1, max: K('K_ATTEMPTS_MAX', 9), step: 1 }, + { key: 'overload', label: say('SAY_OVERLOAD'), min: 0, max: K('K_OVERLOAD_MAX', 5), step: 1 }, + { key: 'last', kind: 'hidden', min: 0, max: SMAX - 1 }, + { key: 'tried', kind: 'hidden', min: 0, max: 4095 }, + { key: 'resting', kind: 'hidden', min: 0, max: 4095 }, +], { names: W.SAY_PRESET_NAMES, values: { size: K('K_PRESET_SIZE', []), last: K('K_PRESET_LAST', []), tried: K('K_PRESET_TRIED', []), resting: K('K_PRESET_RESTING', []), attempts: K('K_PRESET_ATTEMPTS', []), overload: K('K_PRESET_OVERLOAD', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const STEP_TONES = [0, 0, 2, 6, 6] +const FILL = ['#00ff88', '#ffd700', '#ff4d6d'] + +function draw(s) { + const mask = (1 << s.size) - 1 + const tried = s.tried & mask, resting = s.resting & mask & ~tried + const next = nextFree(tried, resting, s.size, s.last < s.size ? s.last : NO_KEY) + const nextPool = nextFree(K('K_POOL_LIST_TRIED', 1), K('K_POOL_LIST_RESTING', 0), K('K_POOL_LIST_SIZE', 2), K('K_POOL_LIST_LAST', 0)) + const st = step(TRANSIENT, s.attempts, next, s.overload, s.size, nextPool) + const out1305 = poolOverloaded(s.overload, s.size) + const state = (i) => (tried & (1 << i) ? 1 : resting & (1 << i) ? 2 : 0) + const cycle = (i) => { + const b = 1 << i, now = state(i) + if (now === 0) ctl.set('tried', s.tried | b) + else if (now === 1) { s.tried &= ~b; ctl.set('resting', s.resting | b) } + else ctl.set('resting', s.resting & ~b) + } + out.replaceChildren( + head(say('SAY_TAP')), + comb([...Array(s.size)].map((_, i) => ({ label: `${i + 1}${i === s.last ? '!' : ''}`, fill: FILL[state(i)], alpha: 0.55, on: i === next })), cycle), + verdict(say('SAY_STEP_NAMES', st), STEP_TONES[st], `${say('SAY_NEXT')}: ${next === NO_KEY ? say('SAY_NONE') : next + 1} · ${say('SAY_LAST')}: ${s.last + 1}`), + tiles([ + { label: say('SAY_NEXT'), value: next === NO_KEY ? say('SAY_NONE') : String(next + 1), tone: 0 }, + { label: say('SAY_POOL_OUT'), value: out1305 ? say('SAY_YES') : say('SAY_NO'), tone: out1305 ? 6 : 0 }, + { label: say('SAY_ATTEMPTS'), value: `${s.attempts} / ${K('K_MAX_ATTEMPTS_PER_TURN', 8)}`, tone: 1 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-night/card.png b/apps/website/public/widgets/hive-night/card.png new file mode 100644 index 0000000000..baf64dc6d4 Binary files /dev/null and b/apps/website/public/widgets/hive-night/card.png differ diff --git a/apps/website/public/widgets/hive-night/hive-night.t27 b/apps/website/public/widgets/hive-night/hive-night.t27 new file mode 100644 index 0000000000..911b0f9805 --- /dev/null +++ b/apps/website/public/widgets/hive-night/hive-night.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-night.t27 -- The night watch: four checks, one verdict, one report +; Source of truth for public/widgets/hive-night/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-night/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/card-chat-night-watch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 25 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps each of four checks between pass, fail and unmeasured and says +; whether an issue is open. The host counts fails and unmeasured; the verdict is night_verdict of +; the counts and the column night_report(verdict, issue open). + +; WHAT IT DOES NOT CLAIM: it runs no check at night: the four results are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_night; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-night"; +pub const TITLE : str = "The night watch: four checks, one verdict, one report"; +pub const DESCRIPTION : str = "Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent."; +pub const IMAGE_ALT : str = "Night watch card: four comb cells, thread green, status door red, merges green, parity green. The verdict reads FAIL; an issue is already open, so the report card sits in the COMMENT column of five: silent, open issue, comment, close issue, leave as is."; +pub const HOOK : str = "Any fail is a fail; else anything unmeasured leaves things as they are. A night that could not measure never closes an issue."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/card-chat-night-watch.t27 (t27 master c42f8fb40): every value on the page is night_verdict and night_report, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 25 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CHECKS : u8 = 4; +pub const K_CARD_CHECK_VERDICTS : u8 = 4; +pub const K_PRESET_C0 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_C1 : [4]u8 = [1, 0, 1, 2]; +pub const K_PRESET_C2 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_C3 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_OPEN : [4]u8 = [1, 1, 0, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A fail, issue open", "All pass, issue open", "A fail, no issue", "Unmeasured"]; +pub const SAY_ASK : str = "The card checks ran overnight. What should the watch do in the morning?"; +pub const SAY_CHECK_NAMES : [4]str = ["thread", "status door", "merges", "parity"]; +pub const SAY_RESULTS : [3]str = ["pass", "fail", "unmeasured"]; +pub const SAY_TAP : str = "tap a check: pass, fail, unmeasured"; +pub const SAY_OPEN : str = "an issue is already open"; +pub const SAY_VERDICT_NAMES : [3]str = ["PASS", "FAIL", "UNMEASURED"]; +pub const SAY_REPORT_NAMES : [5]str = ["silent", "open an issue", "comment", "close the issue", "leave as is"]; +pub const SAY_TONIGHT : str = "tonight"; +pub const SAY_COUNTS : str = "{0} fail, {1} unmeasured"; +pub const SAY_NOTE : str = "A pass with an open issue closes it, so a fixed card does not leave a stale alarm. A night with nothing to say sends nothing."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/card-chat-night-watch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_checks { + assert K_CHECKS == K_CARD_CHECK_VERDICTS; + assert K_CHECKS == 4; +} + +test the_start_has_one_fail { + assert K_PRESET_C1[0] == 1; + assert K_PRESET_OPEN[0] == 1; + assert K_PRESET_C3[3] == 0; +} diff --git a/apps/website/public/widgets/hive-night/index.html b/apps/website/public/widgets/hive-night/index.html new file mode 100644 index 0000000000..d5ae9af979 --- /dev/null +++ b/apps/website/public/widgets/hive-night/index.html @@ -0,0 +1,47 @@ + + + + + + +The night watch: four checks, one verdict, one report · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The night watch: four checks, one verdict, one report

+

Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/card-chat-night-watch.t27 (t27 master c42f8fb40): every value on the page is night_verdict and night_report, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 25 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-night/logic.js b/apps/website/public/widgets/hive-night/logic.js new file mode 100644 index 0000000000..0b2570c9b1 --- /dev/null +++ b/apps/website/public/widgets/hive-night/logic.js @@ -0,0 +1,32 @@ +// Generated by scripts/t27-logic.mjs from card-chat-night-watch.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 5271bf813fbdc58e81e8aff115e75340fc6333d9dfa494ad10f7256ec1baeb68 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-night/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABBwFgAn9/AX8DBAMAAAAEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdHBAZtZW1vcnkCABN0MjdfbmlnaHRfY291bnRzX29rAAAQdDI3X25pZ2h0X3JlcG9ydAABEXQyN19uaWdodF92ZXJkaWN0AAIKWAMOACABIABqQf8BcUEFSQs4AQF/QQQhAgJAIABBAksNAAJAAkAgAA4CAQACC0ECQQEgAUEBcRsPC0EDQQAgAUEBcRshAgsgAgsOAEEBQQJBACABGyAAGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function nightVerdict(fails, unmeasured) { + return W.t27_night_verdict(int(fails, 0, 255, "night_verdict.fails"), int(unmeasured, 0, 255, "night_verdict.unmeasured")) & 255; +} +function nightReport(verdict, issueOpen) { + return W.t27_night_report(int(verdict, 0, 255, "night_report.verdict"), issueOpen ? 1 : 0) & 255; +} +function nightCountsOk(fails, unmeasured) { + return (W.t27_night_counts_ok(int(fails, 0, 255, "night_counts_ok.fails"), int(unmeasured, 0, 255, "night_counts_ok.unmeasured")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + nightCountsOk, + nightReport, + nightVerdict +}; diff --git a/apps/website/public/widgets/hive-night/logic.t27 b/apps/website/public/widgets/hive-night/logic.t27 new file mode 100644 index 0000000000..684b66dce1 --- /dev/null +++ b/apps/website/public/widgets/hive-night/logic.t27 @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// Card Chat Night Watch - verdict and reporting logic +// Part of EPIC #5388 + +module CardChatNightWatch { + // Verdict constants + const CHECK_PASS: u8 = 0 + const CHECK_FAIL: u8 = 1 + const CHECK_UNMEASURED: u8 = 2 + + // Report constants + const REPORT_SILENT: u8 = 0 + const REPORT_OPEN_ISSUE: u8 = 1 + const REPORT_COMMENT: u8 = 2 + const REPORT_CLOSE_ISSUE: u8 = 3 + const REPORT_LEAVE_AS_IS: u8 = 4 + + // Total number of card check verdicts (thread, status door, merges, parity) + const CARD_CHECK_VERDICTS: u8 = 4 + + // Determine night verdict based on failures and unmeasured checks + // Any fail -> FAIL; else any unmeasured -> UNMEASURED; else PASS + fn night_verdict(fails: u8, unmeasured: u8) -> u8 { + if fails > 0 { + return CHECK_FAIL + } else if unmeasured > 0 { + return CHECK_UNMEASURED + } else { + return CHECK_PASS + } + } + + // Determine night report based on verdict and issue state + // FAIL and no open issue -> OPEN_ISSUE + // FAIL and an open issue -> COMMENT + // PASS and an open issue -> CLOSE_ISSUE + // PASS and none -> SILENT + // UNMEASURED -> LEAVE_AS_IS whatever the issue state + // a verdict above 2 -> LEAVE_AS_IS + fn night_report(verdict: u8, issue_open: bool) -> u8 { + if verdict > 2 { + return REPORT_LEAVE_AS_IS + } + + if verdict == CHECK_FAIL { + if issue_open { + return REPORT_COMMENT + } else { + return REPORT_OPEN_ISSUE + } + } else if verdict == CHECK_PASS { + if issue_open { + return REPORT_CLOSE_ISSUE + } else { + return REPORT_SILENT + } + } else { + return REPORT_LEAVE_AS_IS + } + } + + // Check if night counts are acceptable + // true when fails + unmeasured <= CARD_CHECK_VERDICTS + fn night_counts_ok(fails: u8, unmeasured: u8) -> bool { + let total = fails + unmeasured + return total <= CARD_CHECK_VERDICTS + } + + // Tests + test night_verdict_all_pass + let result = night_verdict(0, 0) + assert result == CHECK_PASS + + test night_verdict_any_fail + let result = night_verdict(1, 0) + assert result == CHECK_FAIL + + test night_verdict_fail_beside_unmeasured + let result = night_verdict(1, 3) + assert result == CHECK_FAIL + + test night_verdict_any_unmeasured_no_fail + let result = night_verdict(0, 1) + assert result == CHECK_UNMEASURED + + test night_report_fail_no_issue + let result = night_report(CHECK_FAIL, false) + assert result == REPORT_OPEN_ISSUE + + test night_report_fail_with_issue + let result = night_report(CHECK_FAIL, true) + assert result == REPORT_COMMENT + + test night_report_pass_with_issue + let result = night_report(CHECK_PASS, true) + assert result == REPORT_CLOSE_ISSUE + + test night_report_pass_no_issue + let result = night_report(CHECK_PASS, false) + assert result == REPORT_SILENT + + test night_report_unmeasured_with_issue + let result = night_report(CHECK_UNMEASURED, true) + assert result == REPORT_LEAVE_AS_IS + + test night_report_unmeasured_no_issue + let result = night_report(CHECK_UNMEASURED, false) + assert result == REPORT_LEAVE_AS_IS + + test night_report_invalid_verdict + let result = night_report(5, true) + assert result == REPORT_LEAVE_AS_IS + + test night_counts_ok_within_limit + let result = night_counts_ok(2, 2) + assert result == true + + test night_counts_ok_at_limit + let result = night_counts_ok(4, 0) + assert result == true + + test night_counts_ok_over_limit + let result = night_counts_ok(3, 2) + assert result == false + + test night_counts_ok_zero + let result = night_counts_ok(0, 0) + assert result == true + + // User scenario tests + test scenario_unmeasured_night_with_open_issue + // Given a night where the render answered 401 to every read + // when night_report(night_verdict(0, 4), true) is evaluated + // then it returns REPORT_LEAVE_AS_IS + let verdict = night_verdict(0, 4) + let result = night_report(verdict, true) + assert result == REPORT_LEAVE_AS_IS + + test scenario_fail_night_no_open_issue + // Given a night where the parity check failed and nothing else did + // when night_report(night_verdict(1, 0), false) is evaluated + // then it returns REPORT_OPEN_ISSUE + let verdict = night_verdict(1, 0) + let result = night_report(verdict, false) + assert result == REPORT_OPEN_ISSUE +} \ No newline at end of file diff --git a/apps/website/public/widgets/hive-night/tool.js b/apps/website/public/widgets/hive-night/tool.js new file mode 100644 index 0000000000..ea652a3bb1 --- /dev/null +++ b/apps/website/public/widgets/hive-night/tool.js @@ -0,0 +1,32 @@ +// hive-night/tool.js -- lesson 25 of queen-and-hive: the night watch. The host counts the fails and the +// unmeasured among four checks; the verdict is night_verdict of the counts, the column +// night_report, fn bodies of gHashTag/t27 specs/automation/card-chat-night-watch.t27 +// compiled to wasm (./logic.js). Words are SAY_ and numbers K_ constants of specs/widgets/hive-night.t27. +import { nightReport, nightVerdict } from './logic.js' +import { K, W, ask, board, comb, controls, el, fill, head, root, say, source, verdict } from '../hive-kit.js' + +const N = K('K_CHECKS', 4) +const keys = [...Array(N)].map((_, i) => `c${i}`) +const ctl = controls([ + ...keys.map((key) => ({ key, kind: 'hidden', min: 0, max: 2 })), + { key: 'open', label: say('SAY_OPEN'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { ...Object.fromEntries(keys.map((k, i) => [k, K(`K_PRESET_C${i}`, [])])), open: K('K_PRESET_OPEN', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const RESULT_FILL = ['#00ff88', '#ff4d6d', '#ffd700'] +const VERDICT_TONES = [0, 6, 1] +const REPORT_TONES = [8, 6, 4, 0, 2] + +function draw(s) { + const fails = keys.filter((k) => s[k] === 1).length, unmeasured = keys.filter((k) => s[k] === 2).length + const v = nightVerdict(fails, unmeasured) + const r = nightReport(v, !!s.open) + out.replaceChildren( + head(say('SAY_TAP')), + comb(keys.map((k, i) => ({ label: say('SAY_CHECK_NAMES', i), fill: RESULT_FILL[s[k]], alpha: 0.6 })), (i) => ctl.set(keys[i], (s[keys[i]] + 1) % 3)), + verdict(say('SAY_VERDICT_NAMES', v), VERDICT_TONES[v], fill(say('SAY_COUNTS'), fails, unmeasured)), + board([0, 1, 2, 3, 4].map((i) => ({ name: say('SAY_REPORT_NAMES', i), tone: REPORT_TONES[i] })), [{ col: r, title: say('SAY_TONIGHT'), sub: say('SAY_REPORT_NAMES', r), on: true }])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-price/card.png b/apps/website/public/widgets/hive-price/card.png new file mode 100644 index 0000000000..9a2576ee3c Binary files /dev/null and b/apps/website/public/widgets/hive-price/card.png differ diff --git a/apps/website/public/widgets/hive-price/hive-price.t27 b/apps/website/public/widgets/hive-price/hive-price.t27 new file mode 100644 index 0000000000..42332757b7 --- /dev/null +++ b/apps/website/public/widgets/hive-price/hive-price.t27 @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-price.t27 -- The price of an operation: twice its cost, rounded up +; Source of truth for public/widgets/hive-price/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-price/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/token-wallet.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 20 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the cost of one operation in micro-dollars and a monthly price in +; dollars. The price is price_tokens(cost); the month is month_tokens(dollars), and reels_in and +; portraits_in of that month. + +; WHAT IT DOES NOT CLAIM: it charges nothing; the reel and portrait sizes are the spec constants +; REEL_TOKENS and PORTRAIT_TOKENS. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_price; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-price"; +pub const TITLE : str = "The price of an operation: twice its cost, rounded up"; +pub const DESCRIPTION : str = "Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens."; +pub const IMAGE_ALT : str = "Price card: an operation that costs 12300 micro-dollars sells for 5 tokens. A $20 month buys 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12. The verdict line reads 5 tokens in gold."; +pub const HOOK : str = "One rate everywhere: a token is half a cent, and every operation is sold at twice what it costs, rounded up so nothing is sold below that."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is price_tokens, month_tokens, reels_in and portraits_in, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 20 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_COST_MAX : u32 = 200000; +pub const K_COST_STEP : u16 = 100; +pub const K_USD_MAX : u8 = 200; +pub const K_TOKEN_MICRO_USD : u32 = 5000; +pub const K_MARKUP_PCT : u32 = 200; +pub const K_REEL_TOKENS : u32 = 270; +pub const K_PORTRAIT_TOKENS : u32 = 12; +pub const K_PRESET_COST : [4]u32 = [12300, 2500, 2600, 130000]; +pub const K_PRESET_USD : [4]u8 = [20, 49, 10, 100]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A $0.0123 call, $20 a month", "Exactly half a cent", "Just over half a cent", "A $0.13 lip-sync, $100"]; +pub const SAY_ASK : str = "What does an operation cost a person, and what does a month buy?"; +pub const SAY_COST : str = "what it costs us, micro-dollars"; +pub const SAY_USD : str = "a month, dollars"; +pub const SAY_PRICE : str = "price, tokens"; +pub const SAY_MONTH : str = "tokens a month"; +pub const SAY_REELS : str = "reels in a month"; +pub const SAY_PORTRAITS : str = "portraits in a month"; +pub const SAY_COST_DOLLARS : str = "costs ${0}"; +pub const SAY_EACH : str = "{0} tokens"; +pub const SAY_NOTE : str = "2500 micro-dollars is half a cent: at twice the cost that is exactly one token. 2600 is two, because the price rounds up. Tokens not spent within a month burn: the next lesson."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/token-wallet.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test one_rate_one_markup { + assert K_TOKEN_MICRO_USD == 5000; + assert K_MARKUP_PCT == 200; +} + +test the_presets_fit { + assert K_PRESET_COST[3] <= K_COST_MAX; + assert K_PRESET_USD[3] <= K_USD_MAX; + assert K_PRESET_COST[1] * 2 == K_TOKEN_MICRO_USD; +} diff --git a/apps/website/public/widgets/hive-price/index.html b/apps/website/public/widgets/hive-price/index.html new file mode 100644 index 0000000000..1fef784600 --- /dev/null +++ b/apps/website/public/widgets/hive-price/index.html @@ -0,0 +1,47 @@ + + + + + + +The price of an operation: twice its cost, rounded up · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The price of an operation: twice its cost, rounded up

+

Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is price_tokens, month_tokens, reels_in and portraits_in, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 20 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-price/logic.js b/apps/website/public/widgets/hive-price/logic.js new file mode 100644 index 0000000000..296c7b301e --- /dev/null +++ b/apps/website/public/widgets/hive-price/logic.js @@ -0,0 +1,40 @@ +// Generated by scripts/t27-logic.mjs from token-wallet.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 425a06b53a9250178536c29d0dac0081f36be8f05199bf622f747f96e040fc11 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-price/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABDQJgA39/fwF/YAF/AX8DBgUAAQEBAQQFAXABAQEFAwEAEAYJAX8BQYCAwAALB2IGBm1lbW9yeQIADXQyN19kYXlzX2xlZnQAABB0MjdfcG9ydHJhaXRzX2luAAEMdDI3X3JlZWxzX2luAAIQdDI3X3ByaWNlX3Rva2VucwADEHQyN19tb250aF90b2tlbnMABApHBRsAQQAgAiAAakH//wNxIgAgAWsiAiACIABLGwsHACAAQQxuCwgAIABBjgJuCw8AIABBAXRBhydqQYgnbgsIACAAQcgBbAs="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function monthTokens(usd) { + return W.t27_month_tokens(int(usd, 0, 4294967295, "month_tokens.usd")) >>> 0; +} +function priceTokens(costMicroUsd) { + return W.t27_price_tokens(int(costMicroUsd, 0, 4294967295, "price_tokens.cost_micro_usd")) >>> 0; +} +function reelsIn(tokens) { + return W.t27_reels_in(int(tokens, 0, 4294967295, "reels_in.tokens")) >>> 0; +} +function portraitsIn(tokens) { + return W.t27_portraits_in(int(tokens, 0, 4294967295, "portraits_in.tokens")) >>> 0; +} +function daysLeft(grantedDay, today, life) { + return W.t27_days_left(int(grantedDay, 0, 65535, "days_left.granted_day"), int(today, 0, 65535, "days_left.today"), int(life, 0, 65535, "days_left.life")) & 65535; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + daysLeft, + monthTokens, + portraitsIn, + priceTokens, + reelsIn +}; diff --git a/apps/website/public/widgets/hive-price/logic.t27 b/apps/website/public/widgets/hive-price/logic.t27 new file mode 100644 index 0000000000..9b8ec63538 --- /dev/null +++ b/apps/website/public/widgets/hive-price/logic.t27 @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/token-wallet.t27 -- the token wallet of the agent market: every dollar a +// person pays becomes tokens at ONE rate, every operation is sold at twice what it costs, and +// tokens that are not spent within a month burn. +// Host: 999-multibots-telegraf apps/vibee-editor/packages/vibee-atoms/src/token-wallet.ts +// (the month a package buys, what it is worth in reels and portraits, when it burns), +// read by the mini app's welcome screen and package cards (player/src/pages/Welcome.tsx, +// player/src/components/Agents/PackageCards.tsx) and by render's billing +// (render/src/agent/billing-shared.ts: the rate and the markup every price is made with). +// Generated: `t27c gen-ts` of this file is vendored into the host as +// apps/vibee-editor/packages/vibee-atoms/src/spec/token-wallet.gen.ts. The host imports +// the numbers from there and never restates them; a host test runs this file's vectors +// against the host. +// Why a burn: the owner, 2026-10-01: all money goes into tokens, and the tokens must be spent +// within the month or they burn -- so a person who paid makes content instead of saving +// tokens. Kling and Runway burn a month of plan credits the same way; Kling spends the +// credits that burn soonest first, and so does this. +// Why one rate: every price is made at $0.005 a token, but the Stars packs (render-server.ts +// PACKS) sell a token for 1.1-1.5 Stars, about three times that (tools.ts SALE_USD_PER_TOKEN). +// A package month is sold at THIS rate; moving the Stars packs onto it is the owner's call. +// Claim status: v1 (2026-10-01). The rate, the markup and the start gift are what the host +// already charges. The month, the burn and the spend order are DECIDED but not yet in the +// ledger: render/src/token-ledger.ts keeps one balance with no dates; the batches with an +// expiry are the next change, and the burn must not be sold before it lands. +// Owner decision still open: whether tokens bought on top of a package burn in 30 days like +// the package month (the owner's rule, kept here) or live longer, as Kling's and Runway's +// bought credits do (TOPUP_DAYS is the one line to change). +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::token_wallet { + + pub const KIND : str = "automation"; + pub const ID : str = "token-wallet"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 1; + + // ---- One rate, one markup ----------------------------------------------------------- + pub const TOKEN_MICRO_USD : u32 = 5000; // one token is half a cent, everywhere + pub const MARKUP_PCT : u32 = 200; // an operation is sold at twice what it costs us + pub const MICRO_PER_USD : u32 = 1000000; + + // ---- What a month buys ---------------------------------------------------------------- + // A package's monthly price, in whole dollars, becomes tokens at the one rate: nothing is + // kept back, the margin is the markup on what is spent and whatever burns. + pub const SETUP_INCLUDES_MONTH : bool = true; // the launch price carries the first month + + // ---- The burn ----------------------------------------------------------------------- + pub const MONTH_DAYS : u16 = 30; // a package month burns 30 days after it lands + pub const TOPUP_DAYS : u16 = 30; // owner's rule: bought tokens burn the same way + pub const START_DAYS : u16 = 30; // the start gift burns too + pub const SPEND_ORDER : str = "soonest-burn-first"; + pub const BURNS_BACKDATED : bool = false; // tokens held before v1 never burn + pub const WARN_DAYS_FIRST : u16 = 7; + pub const WARN_DAYS_SECOND : u16 = 3; + pub const WARN_DAYS_LAST : u16 = 1; + + // ---- The start gift ------------------------------------------------------------------- + pub const START_TOKENS : u32 = 20; // granted once, on the first paid call + + // ---- What tokens are worth, in things a person can picture ---------------------------- + // The reference reel is the story reel's own estimate (crm-story-reel.t27 + // PRICE_ESTIMATE_TOKENS: three stills, three lines, 13 s of lip-sync, one cut); the + // portrait is the lead magnet's price (crm-lead-magnet.t27 OP_TOKENS). A host test holds + // both to those specs, so the welcome screen cannot promise a reel at an old price. + pub const REEL_TOKENS : u32 = 270; + pub const PORTRAIT_TOKENS : u32 = 12; + + // Dollars a month to tokens a month. + fn month_tokens(usd: u32) -> u32 { + return usd * MICRO_PER_USD / TOKEN_MICRO_USD; + } + + // An operation's price in tokens from what it costs us, in micro-dollars: rounded up, so + // nothing is sold below twice its cost. + fn price_tokens(cost_micro_usd: u32) -> u32 { + return (cost_micro_usd * MARKUP_PCT / 100 + TOKEN_MICRO_USD - 1) / TOKEN_MICRO_USD; + } + + // Whole reels a number of tokens buys. + fn reels_in(tokens: u32) -> u32 { + return tokens / REEL_TOKENS; + } + + // Whole portraits a number of tokens buys. + fn portraits_in(tokens: u32) -> u32 { + return tokens / PORTRAIT_TOKENS; + } + + // Days left before a batch granted on `granted_day` burns, on `today` (0 when gone). + fn days_left(granted_day: u16, today: u16, life: u16) -> u16 { + if (today >= granted_day + life) { + return 0; + } + return granted_day + life - today; + } + + test "every dollar of a package month becomes tokens at the one rate" { + assert(month_tokens(15) == 3000); + assert(month_tokens(35) == 7000); + assert(month_tokens(50) == 10000); + assert(SETUP_INCLUDES_MONTH); + } + + test "an operation is sold at twice its cost, rounded up" { + assert(MARKUP_PCT == 200); + assert(price_tokens(30000) == 12); + assert(price_tokens(20000) == 8); + assert(price_tokens(1) == 1); + } + + test "a month burns on day thirty and nothing held before it burns" { + assert(days_left(0, 0, MONTH_DAYS) == 30); + assert(days_left(0, 23, MONTH_DAYS) == 7); + assert(days_left(0, 30, MONTH_DAYS) == 0); + assert(TOPUP_DAYS == MONTH_DAYS); + assert(SPEND_ORDER == "soonest-burn-first"); + assert(!BURNS_BACKDATED); + } + + test "the warnings come at seven, three and one day left" { + assert(WARN_DAYS_FIRST > WARN_DAYS_SECOND); + assert(WARN_DAYS_SECOND > WARN_DAYS_LAST); + assert(WARN_DAYS_FIRST < MONTH_DAYS); + } + + test "the start gift buys one portrait and no reel" { + assert(portraits_in(START_TOKENS) == 1); + assert(reels_in(START_TOKENS) == 0); + } + + test "a Blogger month is twenty-five reels" { + assert(reels_in(month_tokens(35)) == 25); + assert(reels_in(month_tokens(15)) == 11); + assert(reels_in(month_tokens(50)) == 37); + assert(portraits_in(month_tokens(35)) == 583); + } +} + +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/apps/website/public/widgets/hive-price/tool.js b/apps/website/public/widgets/hive-price/tool.js new file mode 100644 index 0000000000..7a80c8ff61 --- /dev/null +++ b/apps/website/public/widgets/hive-price/tool.js @@ -0,0 +1,29 @@ +// hive-price/tool.js -- lesson 20 of queen-and-hive: the price of an operation. The price is +// price_tokens(cost), the month month_tokens(dollars), and reels_in and portraits_in of that month, +// fn bodies of gHashTag/t27 specs/automation/token-wallet.t27 compiled to wasm (./logic.js). Words are +// SAY_ and numbers K_ constants of specs/widgets/hive-price.t27. +import { monthTokens, portraitsIn, priceTokens, reelsIn } from './logic.js' +import { K, W, ask, controls, el, fill, root, say, source, tiles, verdict } from '../hive-kit.js' + +const ctl = controls([ + { key: 'cost', label: say('SAY_COST'), min: 0, max: K('K_COST_MAX', 200000), step: K('K_COST_STEP', 100) }, + { key: 'usd', label: say('SAY_USD'), min: 0, max: K('K_USD_MAX', 200), step: 1 }, +], { names: W.SAY_PRESET_NAMES, values: { cost: K('K_PRESET_COST', []), usd: K('K_PRESET_USD', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const price = priceTokens(s.cost) + const month = monthTokens(s.usd) + out.replaceChildren( + verdict(fill(say('SAY_EACH'), price), 1, `${say('SAY_COST')}: ${s.cost}`), + tiles([ + { label: say('SAY_PRICE'), value: String(price), tone: 1 }, + { label: say('SAY_MONTH'), value: String(month), tone: 0 }, + { label: say('SAY_REELS'), value: String(reelsIn(month)), tone: 3 }, + { label: say('SAY_PORTRAITS'), value: String(portraitsIn(month)), tone: 2 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-rank/card.png b/apps/website/public/widgets/hive-rank/card.png new file mode 100644 index 0000000000..6792c5440c Binary files /dev/null and b/apps/website/public/widgets/hive-rank/card.png differ diff --git a/apps/website/public/widgets/hive-rank/hive-rank.t27 b/apps/website/public/widgets/hive-rank/hive-rank.t27 new file mode 100644 index 0000000000..e9d49fa470 --- /dev/null +++ b/apps/website/public/widgets/hive-rank/hive-rank.t27 @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-rank.t27 -- The queue: which issue the Queen takes first +; Source of truth for public/widgets/hive-rank/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-rank/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/priority.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 5 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: seven issues with fixed ages and blockers, in listing order; the reader taps a +; level for each. The host counts the criticals listed before each one and then calls capped_level, +; effective_level, eligible and why; the order is a sort whose only comparison is outranks. + +; WHAT IT DOES NOT CLAIM: the seven issues are fixtures of this page, not the live queue. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_rank; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-rank"; +pub const TITLE : str = "The queue: which issue the Queen takes first"; +pub const DESCRIPTION : str = "Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there."; +pub const IMAGE_ALT : str = "Queue card: seven issues ranked. Three criticals lead, the fourth critical is capped to high, an unlabelled normal comes next, a low issue aged to normal follows it, and one high issue with an open blocker is skipped. Each row carries a why pill: label, capped, listing, aged, blocked."; +pub const HOOK : str = "Labels can inflate: at most 3 criticals run as critical, the fourth runs as high. An issue with an open blocker is skipped, not waited on."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is capped_level, effective_level, eligible, outranks and why, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 5 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_ISSUES : u8 = 7; +pub const K_CRITICAL_CAP : u32 = 3; +pub const K_LEVEL_CRITICAL : u8 = 0; +pub const K_LEVEL_NORMAL : u8 = 2; +pub const K_LEVEL_LOW : u8 = 3; +; The tap state past the lowest level: the issue carries no priority label (SAY_LEVEL_NAMES[4]). +pub const K_NO_LABEL : u8 = 4; +pub const K_AGES : [7]u8 = [1, 3, 6, 8, 20, 30, 2]; +pub const K_BLOCKERS : [7]u8 = [0, 0, 0, 0, 0, 0, 1]; +pub const K_NUMBERS : [7]u16 = [712, 706, 699, 690, 681, 664, 710]; +pub const K_PRESET_L0 : [3]u8 = [0, 1, 0]; +pub const K_PRESET_L1 : [3]u8 = [0, 2, 0]; +pub const K_PRESET_L2 : [3]u8 = [0, 4, 0]; +pub const K_PRESET_L3 : [3]u8 = [0, 4, 0]; +pub const K_PRESET_L4 : [3]u8 = [4, 4, 4]; +pub const K_PRESET_L5 : [3]u8 = [3, 3, 3]; +pub const K_PRESET_L6 : [3]u8 = [1, 4, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["Four criticals", "Few labels", "Two criticals"]; +pub const SAY_ASK : str = "Which open issue does the Queen take first?"; +pub const SAY_LEVEL_NAMES : [5]str = ["critical", "high", "normal", "low", "no label"]; +pub const SAY_WHY_NAMES : [5]str = ["listing", "label", "aged", "capped", "blocked"]; +pub const SAY_ISSUE : str = "#{0} ({1} d open)"; +pub const SAY_TAP : str = "Tap an issue to change its label"; +pub const SAY_ORDER : str = "The order the Queen takes them"; +pub const SAY_SKIPPED : str = "skipped: an open blocker"; +pub const SAY_RUNS : str = "runs as {0}"; +pub const SAY_FIRST : str = "taken first"; +pub const SAY_CAPPED : str = "capped"; +pub const SAY_NOTE : str = "Lower level first, then the real level (an aged issue never beats a real one of its level), then GitHub listing order. An unlabelled issue keeps the place it had before labels existed."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/priority.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test no_label_is_the_state_past_the_lowest_level { + assert K_NO_LABEL == K_LEVEL_LOW + 1; + assert K_LEVEL_CRITICAL < K_LEVEL_NORMAL; +} + +test seven_issues_in_listing_order { + assert K_ISSUES == 7; + assert K_AGES[6] == 2; + assert K_BLOCKERS[6] == 1; +} + +test the_start_has_one_critical_more_than_the_cap { + assert K_PRESET_L3[0] == 0; + assert K_CRITICAL_CAP == 3; + assert K_PRESET_L4[0] == 4; +} diff --git a/apps/website/public/widgets/hive-rank/index.html b/apps/website/public/widgets/hive-rank/index.html new file mode 100644 index 0000000000..90b97d5e44 --- /dev/null +++ b/apps/website/public/widgets/hive-rank/index.html @@ -0,0 +1,47 @@ + + + + + + +The queue: which issue the Queen takes first · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The queue: which issue the Queen takes first

+

Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is capped_level, effective_level, eligible, outranks and why, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 5 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-rank/logic.js b/apps/website/public/widgets/hive-rank/logic.js new file mode 100644 index 0000000000..b48f8555d6 --- /dev/null +++ b/apps/website/public/widgets/hive-rank/logic.js @@ -0,0 +1,40 @@ +// Generated by scripts/t27-logic.mjs from priority.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: d5d5e42e10e89055810a8e8115285f6af4683f8c6e6c36501fa2b4419f4ed070 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-rank/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJgVgBX9/f39/AX9gBn9/f39/fwF/YAF/AX9gA39/fwF/YAJ/fwF/AwYFAAECAwQEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdbBgZtZW1vcnkCAAd0Mjdfd2h5AAAMdDI3X291dHJhbmtzAAEMdDI3X2VsaWdpYmxlAAITdDI3X2VmZmVjdGl2ZV9sZXZlbAADEHQyN19jYXBwZWRfbGV2ZWwABAqyAQUnAAJAIARFDQBBBA8LAkAgASAARg0AQQMPCyADQQFxQQIgAiABRhsLNwEBf0EBIQYCQCAAIANJDQBBACEGIAAgA0sNAEEBIQYgASAESQ0AIAEgBE0gAiAFSXEhBgsgBgsFACAARQs9AAJAAkAgAUEBcUUNACAAQQJJDQBBASEBIABBf2ogAkENSyICTQ0BIAAgAmshAQwBCyAAIQELIAFB/wFxCwwAIAAgAUECSyAAGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function cappedLevel(base, criticalsBefore) { + return W.t27_capped_level(int(base, 0, 255, "capped_level.base"), int(criticalsBefore, 0, 4294967295, "capped_level.criticals_before")) & 255; +} +function effectiveLevel(level, labelled, ageDays) { + return W.t27_effective_level(int(level, 0, 255, "effective_level.level"), labelled ? 1 : 0, int(ageDays, 0, 4294967295, "effective_level.age_days")) & 255; +} +function eligible(openBlockers) { + return (W.t27_eligible(int(openBlockers, 0, 4294967295, "eligible.open_blockers")) & 1) !== 0; +} +function outranks(effA, levelA, indexA, effB, levelB, indexB) { + return (W.t27_outranks(int(effA, 0, 255, "outranks.eff_a"), int(levelA, 0, 255, "outranks.level_a"), int(indexA, 0, 4294967295, "outranks.index_a"), int(effB, 0, 255, "outranks.eff_b"), int(levelB, 0, 255, "outranks.level_b"), int(indexB, 0, 4294967295, "outranks.index_b")) & 1) !== 0; +} +function why(base, level, eff, labelled, openBlockers) { + return W.t27_why(int(base, 0, 255, "why.base"), int(level, 0, 255, "why.level"), int(eff, 0, 255, "why.eff"), labelled ? 1 : 0, int(openBlockers, 0, 4294967295, "why.open_blockers")) & 255; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + cappedLevel, + effectiveLevel, + eligible, + outranks, + why +}; diff --git a/apps/website/public/widgets/hive-rank/logic.t27 b/apps/website/public/widgets/hive-rank/logic.t27 new file mode 100644 index 0000000000..e0a7399a68 --- /dev/null +++ b/apps/website/public/widgets/hive-rank/logic.t27 @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/priority.t27 -- the order the Queen should take open issues in (gHashTag/t27#6366) +; Today the Queen takes the first eligible issue in GitHub's listing order, newest first, and reads no +; label (specs/queen/dispatch.t27, PRIORITY_RULE). This file is the one home of the rule that replaces it. +; Runtimes read it, nobody restates it: the supervisor imports the label vocabulary from `t27c gen-js` +; of this file, `tri priority` calls the functions below through `t27c gen-c` and ctypes. +; Design notes from the survey of agent schedulers (Copilot agent, Devin, OpenHands, Cursor, Jira rank, +; Kubernetes priority queue, Sidekiq weights, stokowski's Linear sort): +; - "no priority" must never sort ahead of "urgent" (stokowski sorted Linear's 0 = none first); +; - an issue with an open blocker is skipped, not waited on (head-of-line blocking); +; - a cap on critical stops priority inflation; aging stops starvation of labelled low work; +; - an issue nobody labelled keeps today's place, so a repository with no labels sees no change. +; Not here: the fair share of bees per level, cooldown after failed attempts, GitHub's typed Priority +; field (read it first once the org enables it; its four values map onto the same levels). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenPriority; + +/// Levels, lower runs first. An unlabelled issue is NORMAL and never ages. +pub const LEVEL_CRITICAL : u8 = 0; +pub const LEVEL_HIGH : u8 = 1; +pub const LEVEL_NORMAL : u8 = 2; +pub const LEVEL_LOW : u8 = 3; + +/// The label vocabulary, comma separated, one string per level. A label in none of them means nothing. +/// When an issue carries labels of two levels, the more urgent one counts. +pub const CRITICAL_LABELS : str = "P0,priority/critical,critical"; +pub const HIGH_LABELS : str = "P1,priority/high,high-priority"; +pub const NORMAL_LABELS : str = "P2,priority/medium"; +pub const LOW_LABELS : str = "P3,P4,priority/low"; + +/// A labelled issue gains one level after this many days open, and at most MAX_AGING_STEPS levels, +/// never CRITICAL. One step, not more: a LOW label that aged past unlabelled work would make "low" +/// the fastest way to the front of a long queue. +pub const AGING_DAYS : u32 = 14; +pub const MAX_AGING_STEPS : u32 = 1; + +/// At most this many issues of one listing run as CRITICAL, taken in listing order; the rest run as HIGH. +pub const CRITICAL_CAP : u32 = 3; + +/// Why an issue sits where it does: the code `tri priority` prints and a supervisor logs. +pub const WHY_LISTING : u8 = 0; +pub const WHY_LABEL : u8 = 1; +pub const WHY_AGED : u8 = 2; +pub const WHY_CAPPED : u8 = 3; +pub const WHY_BLOCKED : u8 = 4; + +/// The level after the critical cap: criticals_before counts CRITICAL issues listed before this one. +pub fn capped_level(base: u8, criticals_before: u32) -> u8 { + if (base == LEVEL_CRITICAL) { + if (criticals_before >= CRITICAL_CAP) { + return LEVEL_HIGH; + } + } + return base; +} + +/// The level an issue runs at: its capped level, raised one step per AGING_DAYS (at most MAX_AGING_STEPS) +/// when it is labelled. +pub fn effective_level(level: u8, labelled: bool, age_days: u32) -> u8 { + if (labelled == false) { + return level; + } + if (level <= LEVEL_HIGH) { + return level; + } + var steps : u32 = age_days / AGING_DAYS; + if (steps > MAX_AGING_STEPS) { + steps = MAX_AGING_STEPS; + } + var gap : u32 = (level as u32) - (LEVEL_HIGH as u32); + if (steps >= gap) { + return LEVEL_HIGH; + } + return ((level as u32) - steps) as u8; +} + +/// An issue with an open blocker is not a candidate; the Queen moves past it. +pub fn eligible(open_blockers: u32) -> bool { + return open_blockers == 0; +} + +/// Whether issue a is taken before issue b: lower effective level, then lower capped level (an aged +/// issue never beats a real one of the same level), then earlier in GitHub's listing. +pub fn outranks(eff_a: u8, level_a: u8, index_a: u32, eff_b: u8, level_b: u8, index_b: u32) -> bool { + if (eff_a < eff_b) { return true; } + if (eff_a > eff_b) { return false; } + if (level_a < level_b) { return true; } + if (level_a > level_b) { return false; } + return index_a < index_b; +} + +/// The reason printed for one issue. +pub fn why(base: u8, level: u8, eff: u8, labelled: bool, open_blockers: u32) -> u8 { + if (open_blockers > 0) { return WHY_BLOCKED; } + if (level != base) { return WHY_CAPPED; } + if (eff != level) { return WHY_AGED; } + if (labelled) { return WHY_LABEL; } + return WHY_LISTING; +} + +test no_label_keeps_the_listing_order { + assert effective_level(LEVEL_NORMAL, false, 400) == LEVEL_NORMAL; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_NORMAL, LEVEL_NORMAL, 1); + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 1, LEVEL_NORMAL, LEVEL_NORMAL, 0) == false; + // #7077: a strict order, so a sort can use it: no issue outranks itself + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 4, LEVEL_NORMAL, LEVEL_NORMAL, 4) == false; +} + +test critical_runs_before_a_newer_unlabelled_issue { + assert outranks(LEVEL_CRITICAL, LEVEL_CRITICAL, 9, LEVEL_NORMAL, LEVEL_NORMAL, 0); +} + +test unlabelled_never_beats_urgent { + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_CRITICAL, LEVEL_CRITICAL, 5) == false; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 0, LEVEL_HIGH, LEVEL_HIGH, 5) == false; +} + +test labelled_work_ages_one_level_and_never_to_critical { + assert effective_level(LEVEL_LOW, true, 13) == LEVEL_LOW; + assert effective_level(LEVEL_LOW, true, 14) == LEVEL_NORMAL; + assert effective_level(LEVEL_LOW, true, 9000) == LEVEL_NORMAL; + assert effective_level(LEVEL_NORMAL, true, 14) == LEVEL_HIGH; + assert effective_level(LEVEL_HIGH, true, 9000) == LEVEL_HIGH; +} + +test an_aged_issue_loses_the_tie_to_a_real_one { + var aged : u8 = effective_level(LEVEL_LOW, true, 28); + assert outranks(aged, LEVEL_LOW, 0, LEVEL_NORMAL, LEVEL_NORMAL, 7) == false; + assert outranks(LEVEL_NORMAL, LEVEL_NORMAL, 7, aged, LEVEL_LOW, 0); + // #7077: of two LOW labels the aged one goes first, whatever the listing says + assert outranks(aged, LEVEL_LOW, 5, LEVEL_LOW, LEVEL_LOW, 0); + assert outranks(LEVEL_LOW, LEVEL_LOW, 0, aged, LEVEL_LOW, 5) == false; +} + +test the_fourth_critical_runs_as_high { + assert capped_level(LEVEL_CRITICAL, 0) == LEVEL_CRITICAL; + assert capped_level(LEVEL_CRITICAL, 2) == LEVEL_CRITICAL; + assert capped_level(LEVEL_CRITICAL, 3) == LEVEL_HIGH; + assert capped_level(LEVEL_LOW, 9) == LEVEL_LOW; +} + +test a_blocked_issue_is_skipped { + assert eligible(0); + assert eligible(1) == false; + assert why(LEVEL_CRITICAL, LEVEL_CRITICAL, LEVEL_CRITICAL, true, 2) == WHY_BLOCKED; +} + +test every_reason_is_reachable { + assert why(LEVEL_NORMAL, LEVEL_NORMAL, LEVEL_NORMAL, false, 0) == WHY_LISTING; + assert why(LEVEL_HIGH, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_LABEL; + assert why(LEVEL_LOW, LEVEL_LOW, LEVEL_NORMAL, true, 0) == WHY_AGED; + assert why(LEVEL_CRITICAL, LEVEL_HIGH, LEVEL_HIGH, true, 0) == WHY_CAPPED; +} + +invariant levels_are_ordered { + assert LEVEL_CRITICAL < LEVEL_HIGH; + assert LEVEL_HIGH < LEVEL_NORMAL; + assert LEVEL_NORMAL < LEVEL_LOW; + assert CRITICAL_CAP > 0; + assert AGING_DAYS > 0; + assert MAX_AGING_STEPS == 1; +} diff --git a/apps/website/public/widgets/hive-rank/tool.js b/apps/website/public/widgets/hive-rank/tool.js new file mode 100644 index 0000000000..af1e7139f1 --- /dev/null +++ b/apps/website/public/widgets/hive-rank/tool.js @@ -0,0 +1,43 @@ +// hive-rank/tool.js -- lesson 5 of queen-and-hive: which open issue the Queen takes first. For each of +// seven issues the host counts the criticals listed before it and calls capped_level, effective_level, +// eligible and why; the order is a sort whose only comparison is outranks. All five are fn bodies of +// gHashTag/t27 specs/queen/priority.t27 compiled to wasm (./logic.js). Words are SAY_ and numbers K_ +// constants of specs/widgets/hive-rank.t27. +import { cappedLevel, effectiveLevel, eligible, outranks, why } from './logic.js' +import { K, W, ask, controls, el, fill, head, ladder, root, say, source, taps } from '../hive-kit.js' + +const N = K('K_ISSUES', 7), AGES = K('K_AGES', []), BLOCK = K('K_BLOCKERS', []), NUM = K('K_NUMBERS', []) +const NO_LABEL = K('K_NO_LABEL', 4), NORMAL = K('K_LEVEL_NORMAL', 2), CRITICAL = K('K_LEVEL_CRITICAL', 0) +const keys = [...Array(N)].map((_, i) => `l${i}`) +const ctl = controls(keys.map((key) => ({ key, kind: 'hidden', min: 0, max: NO_LABEL })), + { names: W.SAY_PRESET_NAMES, values: Object.fromEntries(keys.map((k, i) => [k, K(`K_PRESET_L${i}`, [])])) }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const LEVEL_TONES = [6, 4, 0, 2, 8] +const WHY_TONES = [8, 0, 1, 4, 6] + +function draw(s) { + let criticals = 0 + const rows = keys.map((k, i) => { + const labelled = s[k] !== NO_LABEL + const base = labelled ? s[k] : NORMAL + const capped = cappedLevel(base, criticals) + if (base === CRITICAL) criticals++ + const eff = effectiveLevel(capped, labelled, AGES[i]) + return { i, base, capped, eff, labelled, ok: eligible(BLOCK[i]), why: why(base, capped, eff, labelled, BLOCK[i]) } + }) + const order = rows.filter((r) => r.ok) + for (let a = 1; a < order.length; a++) for (let b = a; b > 0 && outranks(order[b].eff, order[b].capped, order[b].i, order[b - 1].eff, order[b - 1].capped, order[b - 1].i); b--) [order[b], order[b - 1]] = [order[b - 1], order[b]] + const name = (r) => fill(say('SAY_ISSUE'), NUM[r.i], AGES[r.i]) + out.replaceChildren( + head(say('SAY_TAP')), + taps(rows.map((r) => ({ label: name(r), sub: say('SAY_LEVEL_NAMES', s[keys[r.i]]), tone: LEVEL_TONES[s[keys[r.i]]] })), (i) => ctl.set(keys[i], (s[keys[i]] + 1) % (NO_LABEL + 1)), say('SAY_TAP')), + head(say('SAY_ORDER')), + ladder([ + ...order.map((r, n) => ({ label: `${n + 1}. ${name(r)}`, sub: `${fill(say('SAY_RUNS'), say('SAY_LEVEL_NAMES', r.eff))} · ${say('SAY_WHY_NAMES', r.why)}`, tone: WHY_TONES[r.why], on: true })), + ...rows.filter((r) => !r.ok).map((r) => ({ label: name(r), sub: `${say('SAY_SKIPPED')} · ${say('SAY_WHY_NAMES', r.why)}`, tone: 6, on: false })), + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-review-first/card.png b/apps/website/public/widgets/hive-review-first/card.png new file mode 100644 index 0000000000..f550d55aca Binary files /dev/null and b/apps/website/public/widgets/hive-review-first/card.png differ diff --git a/apps/website/public/widgets/hive-review-first/hive-review-first.t27 b/apps/website/public/widgets/hive-review-first/hive-review-first.t27 new file mode 100644 index 0000000000..e3396a37e9 --- /dev/null +++ b/apps/website/public/widgets/hive-review-first/hive-review-first.t27 @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-review-first.t27 -- Review first: no new work while finished work waits +; Source of truth for public/widgets/hive-review-first/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-review-first/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/review_first.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 6 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the review queue from 0 to 40. Four candidates are +; kind_of(assigned, sent back) for each pair of flags, and each sits in the column admission(kind, +; queue) names; the sentence shows when round_refused says a round that admitted only the backlog +; owes it. + +; WHAT IT DOES NOT CLAIM: it does not count a live queue: 29 is the reviewable count the spec +; records for 2026-10-18 17:09Z. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_review_first; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-review-first"; +pub const TITLE : str = "Review first: no new work while finished work waits"; +pub const DESCRIPTION : str = "Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go."; +pub const IMAGE_ALT : str = "Review-first card at 29 dispatches waiting in review. The green ADMIT column holds three cards, sent back, assigned, and assigned and sent back; the red REVIEW FIRST column holds the backlog issue. The line under them reads 29 dispatches wait in review; the backlog is closed until the review queue is empty."; +pub const HOOK : str = "Measured 2026-10-18: nine bees started for every verdict, so the review queue only grew. Now the backlog closes while anything waits in review."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/review_first.t27 (t27 master c42f8fb40): every value on the page is kind_of, admission and round_refused, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 6 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_QUEUE_MAX : u8 = 40; +pub const K_REVIEW_QUEUE_MAX : u32 = 0; +pub const K_PRESET_QUEUE : [3]u8 = [29, 0, 3]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["29 waiting, as measured", "Review queue empty", "3 waiting"]; +pub const SAY_ASK : str = "Should the Queen start new work while finished work waits for a review?"; +pub const SAY_QUEUE : str = "dispatches waiting in review"; +pub const SAY_COLUMNS : [2]str = ["ADMIT", "REVIEW FIRST"]; +pub const SAY_CANDIDATES : [4]str = ["a backlog issue", "sent back by the review", "assigned by the owner", "assigned and sent back"]; +pub const SAY_KIND_NAMES : [3]str = ["backlog", "rework", "assigned"]; +pub const SAY_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty"; +pub const SAY_OPEN : str = "The backlog is open: the queue is empty."; +pub const SAY_ROUND : str = "a round with only backlog candidates says"; +pub const SAY_NOTE : str = "Rework is never backlog: the review asked for it. The queue empties on its own, because every outcome of a review that is not an accept has a clock (review_valve.t27)."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/review_first.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_owners_bound_is_zero { + assert K_REVIEW_QUEUE_MAX == 0; + assert K_PRESET_QUEUE[1] == K_REVIEW_QUEUE_MAX; +} + +test the_start_is_the_measured_queue { + assert K_PRESET_QUEUE[0] == 29; + assert K_PRESET_QUEUE[0] <= K_QUEUE_MAX; +} diff --git a/apps/website/public/widgets/hive-review-first/index.html b/apps/website/public/widgets/hive-review-first/index.html new file mode 100644 index 0000000000..9a78234be4 --- /dev/null +++ b/apps/website/public/widgets/hive-review-first/index.html @@ -0,0 +1,47 @@ + + + + + + +Review first: no new work while finished work waits · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Review first: no new work while finished work waits

+

Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/review_first.t27 (t27 master c42f8fb40): every value on the page is kind_of, admission and round_refused, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 6 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-review-first/logic.js b/apps/website/public/widgets/hive-review-first/logic.js new file mode 100644 index 0000000000..1e924db4b9 --- /dev/null +++ b/apps/website/public/widgets/hive-review-first/logic.js @@ -0,0 +1,44 @@ +// Generated by scripts/t27-logic.mjs from review_first.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: c0ec4028feccb9740ddd434f8a1ee8f4fce9b7a4ce2dd1664de504a0b5d06e88 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-review-first/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABFQNgAn9/AX9gAX8Bf2AFf39/f38BfwMHBgAAAAECAAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB3IHBm1lbW9yeQIAEXQyN19yb3VuZF9yZWZ1c2VkAAAKdDI3X2FkbWl0cwABDXQyN19hZG1pc3Npb24AAhB0MjdfYmFja2xvZ19vcGVuAAMTdDI3X2luX3Jldmlld19xdWV1ZQAEC3QyN19raW5kX29mAAUKZwYLACAARSABQQBHcQsSACAAQX9qQf8BcUECSSABRXILFQAgAEF9akH/AXFB/gFJIAFBAEdxCwUAIABFCxoAIAAgAXEgA0F/c3EgBHEgAkH/AXFBAklxCw8AQQIgAUEBcSAAQQFxGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function kindOf(assigned, sentBack) { + return W.t27_kind_of(assigned ? 1 : 0, sentBack ? 1 : 0) & 255; +} +function inReviewQueue(started, finished, state, reaped, issueOpen) { + return (W.t27_in_review_queue(started ? 1 : 0, finished ? 1 : 0, int(state, 0, 255, "in_review_queue.state"), reaped ? 1 : 0, issueOpen ? 1 : 0) & 1) !== 0; +} +function backlogOpen(reviewQueue) { + return (W.t27_backlog_open(int(reviewQueue, 0, 4294967295, "backlog_open.review_queue")) & 1) !== 0; +} +function admission(kind, reviewQueue) { + return W.t27_admission(int(kind, 0, 255, "admission.kind"), int(reviewQueue, 0, 4294967295, "admission.review_queue")) & 255; +} +function admits(kind, reviewQueue) { + return (W.t27_admits(int(kind, 0, 255, "admits.kind"), int(reviewQueue, 0, 4294967295, "admits.review_queue")) & 1) !== 0; +} +function roundRefused(admitted, refused) { + return (W.t27_round_refused(int(admitted, 0, 4294967295, "round_refused.admitted"), int(refused, 0, 4294967295, "round_refused.refused")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + admission, + admits, + backlogOpen, + inReviewQueue, + kindOf, + roundRefused +}; diff --git a/apps/website/public/widgets/hive-review-first/logic.t27 b/apps/website/public/widgets/hive-review-first/logic.t27 new file mode 100644 index 0000000000..dfce350f1d --- /dev/null +++ b/apps/website/public/widgets/hive-review-first/logic.t27 @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/review_first.t27 -- no new work from the backlog while finished work waits in review (gHashTag/t27#10555). +; The owner's rule, 2026-10-18: the Queen takes nothing from the backlog while there is a queue in review. +; Measured 2026-10-18 17:09Z on api.t27.ai/queen/status: 69 of 70 workers busy; in the last hour 102 +; dispatched, 56 finished, 11 reviewed; reviewable (the review sweep's own queue) 29, unreviewed 362. +; The swarm starts about nine bees for every verdict, so the queue in review only grows, and each new bee +; holds files (claim.t27) that the bees already in review need when they are sent back. +; The rule: +; 1. the review queue is exactly the review sweep's own filter, the one the status page counts as +; `reviewable`: the dispatch started, it finished, its review_state is NULL or 'wait', it was not +; reaped, and its issue is still open (in_review_queue); +; 2. while that queue is longer than REVIEW_QUEUE_MAX (0: not while anything waits) a candidate from +; the backlog is refused, and the round says why (admission, R_REVIEW_FIRST); +; 3. work the review itself asked for is never backlog: an issue the review sent back is dispatched +; again at any queue length (KIND_REWORK), and so is an issue the owner assigned by hand +; (KIND_ASSIGNED, the supervisor's pendingAssigns); +; 4. the queue empties on its own: every outcome of the review that is not an accept has a clock +; (review_valve.t27), so a closed backlog always reopens without a person. +; The host counts the queue and names each candidate's kind; this file decides. Not here: the wiring into +; trios (queen-tick.ts candidate filter, vendored wasm and t27c gen-ts constants), how keys are shared +; between bees and the reviewer, the review verdicts themselves. ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenReviewFirst; + +; --- 1. What a candidate is --------------------------------------------------------------------------- + +/// KIND_BACKLOG: an open issue with no dispatch the review is working on. KIND_REWORK: an issue whose +/// last dispatch the review sent back (review_state 'sendBack'). KIND_ASSIGNED: an issue the owner +/// assigned by hand. The codes are mirrored by number: a new kind takes the next code, none moves. +pub const KIND_BACKLOG : u8 = 0; +pub const KIND_REWORK : u8 = 1; +pub const KIND_ASSIGNED : u8 = 2; + +/// The owner's hand wins over the review's send-back, and both win over the backlog. +pub fn kind_of(assigned: bool, sent_back: bool) -> u8 { + if (assigned) { return KIND_ASSIGNED; } + if (sent_back) { return KIND_REWORK; } + return KIND_BACKLOG; +} + +; --- 2. What the review queue is ---------------------------------------------------------------------- + +/// A dispatch row's review_state as the host reads it: RS_NONE is NULL (no verdict yet), RS_WAIT is +/// 'wait' (fewer verdicts than criteria, dispatch.t27 REVIEW_RULE), RS_OTHER is any written verdict. +pub const RS_NONE : u8 = 0; +pub const RS_WAIT : u8 = 1; +pub const RS_OTHER : u8 = 2; + +/// Whether one dispatch row is in the review queue: the sweep's own filter (trios queen-tick.ts +/// reviewFinishedDispatches, counted as `reviewable` by queen-public-status.ts). A row whose issue was +/// closed is never reviewed, so it never holds the backlog shut. +pub fn in_review_queue(started: bool, finished: bool, state: u8, reaped: bool, issue_open: bool) -> bool { + if (started == false) { return false; } + if (finished == false) { return false; } + if (reaped) { return false; } + if (issue_open == false) { return false; } + if (state == RS_NONE) { return true; } + return state == RS_WAIT; +} + +; --- 3. Admission ------------------------------------------------------------------------------------- + +/// How many dispatches may wait in review while the backlog stays open. Zero, the owner's words: not +/// while anything is in review. +pub const REVIEW_QUEUE_MAX : u32 = 0; + +/// R_ADMIT: the candidate may be chosen. R_REVIEW_FIRST: refused, the backlog is closed while the review +/// queue is longer than REVIEW_QUEUE_MAX. +pub const R_ADMIT : u8 = 0; +pub const R_REVIEW_FIRST : u8 = 1; + +/// The refusal as the round reports it, after the queue length: "29 dispatches wait in review; ...". +pub const REFUSAL_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty"; + +pub fn backlog_open(review_queue: u32) -> bool { + return review_queue <= REVIEW_QUEUE_MAX; +} + +pub fn admission(kind: u8, review_queue: u32) -> u8 { + if (kind == KIND_REWORK) { return R_ADMIT; } + if (kind == KIND_ASSIGNED) { return R_ADMIT; } + if (backlog_open(review_queue)) { return R_ADMIT; } + return R_REVIEW_FIRST; +} + +pub fn admits(kind: u8, review_queue: u32) -> bool { + return admission(kind, review_queue) == R_ADMIT; +} + +/// Whether a round that admitted no candidate owes the review-first sentence: some candidate was +/// refused by this rule and none was admitted. +pub fn round_refused(admitted: u32, refused: u32) -> bool { + if (admitted > 0) { return false; } + return refused > 0; +} + +; --- 4. Tests ----------------------------------------------------------------------------------------- + +test the_owner_hand_wins_then_the_review_then_the_backlog { + assert kind_of(false, false) == 0; + assert kind_of(false, true) == 1; + assert kind_of(true, false) == 2; + assert kind_of(true, true) == 2; +} + +test the_queue_is_the_sweeps_own_filter { + assert in_review_queue(true, true, RS_NONE, false, true) == true; + assert in_review_queue(true, true, RS_WAIT, false, true) == true; + assert in_review_queue(true, true, RS_OTHER, false, true) == false; + assert in_review_queue(true, false, RS_NONE, false, true) == false; + assert in_review_queue(false, true, RS_NONE, false, true) == false; + assert in_review_queue(true, true, RS_NONE, true, true) == false; + assert in_review_queue(true, true, RS_NONE, false, false) == false; + assert in_review_queue(true, true, RS_WAIT, true, true) == false; + assert in_review_queue(true, true, 7, false, true) == false; +} + +test the_backlog_is_closed_while_anything_waits_in_review { + assert admission(KIND_BACKLOG, 0) == 0; + assert admission(KIND_BACKLOG, 1) == 1; + assert admission(KIND_BACKLOG, 29) == 1; + assert admission(KIND_BACKLOG, 4294967295) == 1; + assert backlog_open(0) == true; + assert backlog_open(1) == false; + assert admits(KIND_BACKLOG, 0) == true; + assert admits(KIND_BACKLOG, 1) == false; +} + +test work_the_review_asked_for_is_never_backlog { + assert admission(KIND_REWORK, 0) == 0; + assert admission(KIND_REWORK, 1) == 0; + assert admission(KIND_REWORK, 29) == 0; + assert admission(KIND_REWORK, 4294967295) == 0; + assert admits(KIND_REWORK, 362) == true; +} + +test the_owners_assignment_is_never_held { + assert admission(KIND_ASSIGNED, 0) == 0; + assert admission(KIND_ASSIGNED, 29) == 0; + assert admits(KIND_ASSIGNED, 4294967295) == true; +} + +test an_unknown_kind_is_backlog_and_is_held { + assert admission(3, 0) == 0; + assert admission(3, 1) == 1; + assert admission(255, 29) == 1; +} + +test a_round_reports_the_rule_only_when_it_stopped_everything { + assert round_refused(0, 0) == false; + assert round_refused(0, 5) == true; + assert round_refused(1, 5) == false; + assert round_refused(3, 0) == false; +} + +test the_measured_round_of_2026_10_18 { + assert admits(kind_of(false, false), 29) == false; + assert admits(kind_of(false, true), 29) == true; + assert admits(kind_of(true, false), 29) == true; + assert admits(kind_of(false, false), 0) == true; +} + +; --- 5. Invariants ------------------------------------------------------------------------------------ + +invariant the_codes_never_move { + assert KIND_BACKLOG == 0 and KIND_REWORK == 1 and KIND_ASSIGNED == 2; + assert RS_NONE == 0 and RS_WAIT == 1 and RS_OTHER == 2; + assert R_ADMIT == 0 and R_REVIEW_FIRST == 1; +} + +/// The owner's bound: nothing may wait in review while the backlog is open. +invariant the_bound_is_the_owners { + assert REVIEW_QUEUE_MAX == 0; +} diff --git a/apps/website/public/widgets/hive-review-first/tool.js b/apps/website/public/widgets/hive-review-first/tool.js new file mode 100644 index 0000000000..da4c6c0315 --- /dev/null +++ b/apps/website/public/widgets/hive-review-first/tool.js @@ -0,0 +1,30 @@ +// hive-review-first/tool.js -- lesson 6 of queen-and-hive: no new work while finished work waits in +// review. Each candidate is kind_of its two flags and sits in the column admission(kind, queue) names; +// the sentence shows when round_refused says so. All three are fn bodies of gHashTag/t27 +// specs/queen/review_first.t27 compiled to wasm (./logic.js). Words are SAY_ and numbers K_ constants +// of specs/widgets/hive-review-first.t27. +import { admission, kindOf, roundRefused } from './logic.js' +import { K, W, ask, board, controls, el, head, root, say, source, verdict } from '../hive-kit.js' + +const FLAGS = [[false, false], [false, true], [true, false], [true, true]] +const ctl = controls([ + { key: 'queue', label: say('SAY_QUEUE'), min: 0, max: K('K_QUEUE_MAX', 40), step: 1 }, +], { names: W.SAY_PRESET_NAMES, values: { queue: K('K_PRESET_QUEUE', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const cards = FLAGS.map(([assigned, sentBack], i) => { + const kind = kindOf(assigned, sentBack) + return { col: admission(kind, s.queue), title: say('SAY_CANDIDATES', i), sub: say('SAY_KIND_NAMES', kind), tone: admission(kind, s.queue) ? 6 : 0 } + }) + const backlog = admission(kindOf(false, false), s.queue) + const says = roundRefused(backlog ? 0 : 1, backlog ? 1 : 0) + out.replaceChildren( + board([{ name: say('SAY_COLUMNS', 0), tone: 0 }, { name: say('SAY_COLUMNS', 1), tone: 6 }], cards), + head(say('SAY_ROUND')), + says ? verdict(`${s.queue}${say('SAY_TAIL')}`, 6) : verdict(say('SAY_OPEN'), 0)) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-reviewers/card.png b/apps/website/public/widgets/hive-reviewers/card.png new file mode 100644 index 0000000000..36fee3c5af Binary files /dev/null and b/apps/website/public/widgets/hive-reviewers/card.png differ diff --git a/apps/website/public/widgets/hive-reviewers/hive-reviewers.t27 b/apps/website/public/widgets/hive-reviewers/hive-reviewers.t27 new file mode 100644 index 0000000000..7c48d0930d --- /dev/null +++ b/apps/website/public/widgets/hive-reviewers/hive-reviewers.t27 @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-reviewers.t27 -- How many reviewer bees: keeping up with the finish rate +; Source of truth for public/widgets/hive-reviewers/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-reviewers/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 10 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets keys, lanes per key, free memory, memory per review, rows waiting +; and rows finished an hour. The upper meter is reviewer_concurrency of the top five, the lower +; slots_needed(finished an hour), and the gate keeps_up of the two. + +; WHAT IT DOES NOT CLAIM: the start numbers are the ones the spec records for 2026-10-09; the page +; measures nothing. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_reviewers; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-reviewers"; +pub const TITLE : str = "How many reviewer bees: keeping up with the finish rate"; +pub const DESCRIPTION : str = "Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work."; +pub const IMAGE_ALT : str = "Reviewer card at the load of 2026-10-09: 111 rows waiting, 58 finished an hour, 4 keys of 1 lane. Meters read 4 reviews at once against 5 needed; the gate is shut, the reviewer falls behind. The fixed bound of 4 is marked on the meter."; +pub const HOOK : str = "At 300 s a review, one slot clears 12 rows an hour. The fixed 4 tops out at 48 while bees finish 58, so the backlog only grew. 16 lanes keep up."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is reviewer_concurrency, slots_needed and keeps_up, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 10 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KEYS_MAX : u8 = 20; +pub const K_LANES_MAX : u8 = 4; +pub const K_MB_MAX : u16 = 8000; +pub const K_MB_STEP : u16 = 250; +pub const K_PER_MAX : u16 = 1000; +pub const K_PER_STEP : u8 = 50; +pub const K_WAITING_MAX : u8 = 150; +pub const K_RATE_MAX : u8 = 120; +pub const K_REVIEWER_CONCURRENCY : u32 = 4; +pub const K_REVIEWER_CEILING : u32 = 16; +pub const K_REVIEW_ROW_SECONDS : u32 = 300; +pub const K_PRESET_KEYS : [4]u8 = [4, 10, 4, 4]; +pub const K_PRESET_LANES : [4]u8 = [1, 2, 1, 1]; +pub const K_PRESET_MB : [4]u16 = [8000, 8000, 1000, 8000]; +pub const K_PRESET_PER : [4]u16 = [500, 500, 500, 500]; +pub const K_PRESET_WAITING : [4]u8 = [111, 111, 111, 0]; +pub const K_PRESET_RATE : [4]u8 = [58, 58, 58, 58]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["As measured, 4 keys", "10 keys of 2 lanes", "Short of memory", "Nothing waits"]; +pub const SAY_ASK : str = "How many reviews must run at once so finished work does not pile up?"; +pub const SAY_KEYS : str = "model keys"; +pub const SAY_LANES : str = "lanes per key"; +pub const SAY_MB : str = "free memory, MB"; +pub const SAY_PER : str = "MB per review"; +pub const SAY_WAITING : str = "rows waiting"; +pub const SAY_RATE : str = "rows finished an hour"; +pub const SAY_AT_ONCE : str = "reviews at once"; +pub const SAY_NEEDED : str = "needed to keep up"; +pub const SAY_FIXED : str = "the fixed bound of {0}"; +pub const SAY_KEEPS : str = "The reviewer keeps up"; +pub const SAY_BEHIND : str = "The reviewer falls behind"; +pub const SAY_GAP : str = "{0} at once, {1} needed"; +pub const SAY_NOTE : str = "0 only when nothing waits, at least 1 while anything waits, never above 16. Each review takes a lane no bee holds and runs on its own clock, so the round no longer reviews at all."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/reviewer.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_bounds_are_the_specs { + assert K_REVIEWER_CONCURRENCY == 4; + assert K_REVIEWER_CEILING == 16; + assert K_REVIEW_ROW_SECONDS == 300; +} + +test the_start_is_the_measured_load { + assert K_PRESET_WAITING[0] == 111; + assert K_PRESET_RATE[0] == 58; + assert K_PRESET_KEYS[0] == K_REVIEWER_CONCURRENCY; +} diff --git a/apps/website/public/widgets/hive-reviewers/index.html b/apps/website/public/widgets/hive-reviewers/index.html new file mode 100644 index 0000000000..cb8da0155e --- /dev/null +++ b/apps/website/public/widgets/hive-reviewers/index.html @@ -0,0 +1,47 @@ + + + + + + +How many reviewer bees: keeping up with the finish rate · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

How many reviewer bees: keeping up with the finish rate

+

Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is reviewer_concurrency, slots_needed and keeps_up, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 10 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-reviewers/logic.js b/apps/website/public/widgets/hive-reviewers/logic.js new file mode 100644 index 0000000000..19564c78b0 --- /dev/null +++ b/apps/website/public/widgets/hive-reviewers/logic.js @@ -0,0 +1,60 @@ +// Generated by scripts/t27-logic.mjs from reviewer.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 0b69832d9d471757488d65e36935f088efa6d4c976bc04e58ce9d557698a8063 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-reviewers/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJAVgA39/fwF/YAJ/fwF/YAF/AX9gBH9/f38Bf2AFf39/f38BfwMNDAABAgICAwEAAQICBAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB98BCwZtZW1vcnkCABZ0Mjdfbm9fYnVuZGxlX2lzX2VtcHR5AAANdDI3X2R1ZV9hZ2FpbgABGXQyN19yZXZpc2l0X2FmdGVyX3NlY29uZHMAAxF0Mjdfcm91bmRfcmV2aWV3cwAED3QyN192aXNpdF9maXJzdAAFEHQyN19yZXZpZXdfc2xvdHMABhd0MjdfcmV2aWV3X3Nsb3RzX3dpdGhpbgAHDHQyN19rZWVwc191cAAIEHQyN19zbG90c19uZWVkZWQAChh0MjdfcmV2aWV3ZXJfY29uY3VycmVuY3kACwqoAwwiAQF/QQAhAwJAIABBAXFFDQAgAUF/cyACQQlLcSEDCyADCw0AIAEgABCCgICAAE8LTgECfwJAIAANAEEADwtBHiEBQQEhAgN/AkACQCAAIAJGDQAgAkEGRw0BQYQHDwsgAUGEByABQYQHSRsPCyACQQFqIQIgAUEBdCEBDAALCwoAIAAQgoCAgAALBwAgAEEBcwswAQF/QQEhBAJAAkAgAEEBcUUNACACQQFxRQ0BCyAAIAJBf3NyIAEgA0txIQQLIAQLGwEBfyABQQQgAGsiAiABIAJJG0EAIABBBEkbCxsBAX8gAiAAIAFrIgMgAiADSRtBACAAIAFLGwsNACAAIAEQiYCAgABPCzIBAX8gAEGQHG4iAUGsAmwgACABQZAcbGsiAEH//wNxQQxuaiAAQawCbEGQHHBBAEdqCwoAIAAQiYCAgAALXgACQCAEDQBBAA8LIAFBECABQRBJGyAAQRAgAEEQSRtsIQBBASEBAkAgA0UNACACIANuIQELIAAgASAAIAFJGyIBIAQgASAESRsiBEEBIARBAUsbIgRBECAEQRBJGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function reviewerConcurrency(keys, lanesPerKey, freeMb, mbPerReview, waiting) { + return W.t27_reviewer_concurrency(int(keys, 0, 4294967295, "reviewer_concurrency.keys"), int(lanesPerKey, 0, 4294967295, "reviewer_concurrency.lanes_per_key"), int(freeMb, 0, 4294967295, "reviewer_concurrency.free_mb"), int(mbPerReview, 0, 4294967295, "reviewer_concurrency.mb_per_review"), int(waiting, 0, 4294967295, "reviewer_concurrency.waiting")) >>> 0; +} +function slotsNeeded(finishedPerHour) { + return W.t27_slots_needed(int(finishedPerHour, 0, 4294967295, "slots_needed.finished_per_hour")) >>> 0; +} +function keepsUp(slots, finishedPerHour) { + return (W.t27_keeps_up(int(slots, 0, 4294967295, "keeps_up.slots"), int(finishedPerHour, 0, 4294967295, "keeps_up.finished_per_hour")) & 1) !== 0; +} +function reviewSlotsWithin(bound, inFlight, waiting) { + return W.t27_review_slots_within(int(bound, 0, 4294967295, "review_slots_within.bound"), int(inFlight, 0, 4294967295, "review_slots_within.in_flight"), int(waiting, 0, 4294967295, "review_slots_within.waiting")) >>> 0; +} +function reviewSlots(inFlight, waiting) { + return W.t27_review_slots(int(inFlight, 0, 4294967295, "review_slots.in_flight"), int(waiting, 0, 4294967295, "review_slots.waiting")) >>> 0; +} +function visitFirst(neverA, secondsSinceA, neverB, secondsSinceB) { + return (W.t27_visit_first(neverA ? 1 : 0, int(secondsSinceA, 0, 4294967295, "visit_first.seconds_since_a"), neverB ? 1 : 0, int(secondsSinceB, 0, 4294967295, "visit_first.seconds_since_b")) & 1) !== 0; +} +function roundReviews(reviewerRunning) { + return (W.t27_round_reviews(reviewerRunning ? 1 : 0) & 1) !== 0; +} +function revisitAfterSeconds(waits) { + return W.t27_revisit_after_seconds(int(waits, 0, 4294967295, "revisit_after_seconds.waits")) >>> 0; +} +function dueAgain(waits, secondsSinceVisit) { + return (W.t27_due_again(int(waits, 0, 4294967295, "due_again.waits"), int(secondsSinceVisit, 0, 4294967295, "due_again.seconds_since_visit")) & 1) !== 0; +} +function noBundleIsEmpty(onRunner, bundleStored, minutesSinceFinished) { + return (W.t27_no_bundle_is_empty(onRunner ? 1 : 0, bundleStored ? 1 : 0, int(minutesSinceFinished, 0, 4294967295, "no_bundle_is_empty.minutes_since_finished")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + dueAgain, + keepsUp, + noBundleIsEmpty, + reviewSlots, + reviewSlotsWithin, + reviewerConcurrency, + revisitAfterSeconds, + roundReviews, + slotsNeeded, + visitFirst +}; diff --git a/apps/website/public/widgets/hive-reviewers/logic.t27 b/apps/website/public/widgets/hive-reviewers/logic.t27 new file mode 100644 index 0000000000..dcc617112b --- /dev/null +++ b/apps/website/public/widgets/hive-reviewers/logic.t27 @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time +; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the +; live Queen (gHashTag/BrowserOS trios/agent-server): +; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91); +; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after +; another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to +; 2 minutes -- in practice about one review a round; +; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min). +; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each +; on its own lane, one row never twice at once, and every waiting row visited in turn. The round +; then reviews nothing: two sweeps over one row would race on its verdict. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenReviewer; + +/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy +/// costs one query. +pub const REVIEWER_EVERY_SECONDS : u32 = 10; + +/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the +/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds). +/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays +/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour. +pub const REVIEWER_CONCURRENCY : u32 = 4; + +/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and +/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`). +pub const REVIEWER_CEILING : u32 = 16; + +/// Seconds in an hour; finish rates are measured per hour. +pub const SECONDS_PER_HOUR : u32 = 3600; + +/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer +/// shares a 45 s budget with the round, because it no longer holds the round up. +pub const REVIEW_ROW_SECONDS : u32 = 300; + +/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58 +/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS +/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below +/// the finish rate even if every review used its whole budget, and the backlog only grows. +/// How many rows the reviewer may take at once, from what the host measures: +/// - `keys` model keys, each with `lanes_per_key` lanes free for review; +/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review); +/// - never above REVIEWER_CEILING, never above the rows `waiting`; +/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains. +pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 { + if (waiting == 0) { return 0; } + var k : u32 = keys; + if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; } + var l : u32 = lanes_per_key; + if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; } + var bound : u32 = k * l; + if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; } + var by_memory : u32 = 1; + if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; } + if (by_memory < bound) { bound = by_memory; } + if (waiting < bound) { bound = waiting; } + if (bound < 1) { return 1; } + return bound; +} + +/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every +/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600). +/// Split by the hour so no u32 product overflows. +pub fn slots_needed(finished_per_hour: u32) -> u32 { + let whole : u32 = finished_per_hour / SECONDS_PER_HOUR; + let part : u32 = finished_per_hour % SECONDS_PER_HOUR; + let part_seconds : u32 = part * REVIEW_ROW_SECONDS; + var extra : u32 = part_seconds / SECONDS_PER_HOUR; + if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; } + return whole * REVIEW_ROW_SECONDS + extra; +} + +/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows. +pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool { + return slots >= slots_needed(finished_per_hour); +} + +/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but +/// no more than the rows waiting. +pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 { + if (in_flight >= bound) { return 0; } + let room : u32 = bound - in_flight; + if (waiting < room) { return waiting; } + return room; +} + +/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed +/// REVIEWER_CONCURRENCY, unchanged for its existing callers. +pub fn review_slots(in_flight: u32, waiting: u32) -> u32 { + return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting); +} + +/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never +/// visited before any it has. Without it, a row that is re-read and waits again (no review spent) +/// keeps its place at the head and the reviewer circles the same few rows. +pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool { + if (never_a and never_b == false) { return true; } + if (never_b and never_a == false) { return false; } + return seconds_since_a > seconds_since_b; +} + +/// While the reviewer runs, the round reviews nothing. +pub fn round_reviews(reviewer_running: bool) -> bool { + return reviewer_running == false; +} + +/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen, +/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could +/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row +/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned +/// about 430 visits of the reviewer's slots. +/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never +/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that +/// has started to read is visited at the normal pace again. +pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30; +pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900; + +pub fn revisit_after_seconds(waits: u32) -> u32 { + if (waits == 0) { return 0; } + var wait : u32 = WAIT_REVISIT_BASE_SECONDS; + var n : u32 = 1; + while (n < waits) { + if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + return wait; +} + +/// Whether a row may be taken now: one that never waited always may, and one that waited only after +/// its revisit time has passed since the last visit. +pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool { + return seconds_since_visit >= revisit_after_seconds(waits); +} + +/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the +/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait` +/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only +/// when the bee committed something, so a bee that committed nothing leaves no branch here, the +/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed. +/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free +/// again after the retry floor. +/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle +/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit, +/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty +/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits, +/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner` +/// false) and a row whose bundle is stored are never decided here: their diff failed for some +/// other reason, and that still decides nothing. +pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10; + +pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool { + if (on_runner == false) { return false; } + if (bundle_stored) { return false; } + return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES; +} + +test a_pass_fills_the_free_slots_with_waiting_rows { + assert review_slots(0, 0) == 0; + assert review_slots(0, 2) == 2; + assert review_slots(0, 100) == REVIEWER_CONCURRENCY; + assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1; + assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1; + assert review_slots(REVIEWER_CONCURRENCY, 100) == 0; + assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0; +} + +test the_bound_follows_lanes_memory_and_backlog { + // nothing waits: no slot, whatever the host has + assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0; + // lanes bound: 2 keys x 3 lanes + assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6; + // memory bound: 1000 MB at 300 MB a review + assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3; + // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free + assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16; + assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING; + // never above the backlog + assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2; + // at least one while anything waits, even with no lane or memory measured + assert reviewer_concurrency(0, 0, 0, 0, 1) == 1; + assert reviewer_concurrency(10, 10, 100, 300, 111) == 1; + assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1; + assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1; +} + +test the_measured_load_needs_five_and_the_bound_admits_them { + // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5 + assert slots_needed(58) == 5; + assert slots_needed(0) == 0; + assert slots_needed(12) == 1; + assert slots_needed(13) == 2; + assert slots_needed(3600) == REVIEW_ROW_SECONDS; + assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1; + assert slots_needed(4294967295) == 357913942; + // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true; + assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16; + assert keeps_up(4, 48) == true; + assert keeps_up(4, 49) == false; +} + +test a_pass_fills_the_free_slots_against_the_computed_bound { + let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111); + assert review_slots_within(bound, 0, 111) == 16; + assert review_slots_within(bound, 11, 111) == 5; + assert review_slots_within(bound, 15, 3) == 1; + assert review_slots_within(bound, 16, 111) == 0; + assert review_slots_within(bound, 17, 111) == 0; + assert review_slots_within(bound, 0, 3) == 3; + assert review_slots_within(0, 0, 111) == 0; +} + +test every_row_is_visited_in_turn { + // never visited goes first + assert visit_first(true, 0, false, 9999) == true; + assert visit_first(false, 9999, true, 0) == false; + // otherwise the one visited longest ago + assert visit_first(false, 300, false, 20) == true; + assert visit_first(false, 20, false, 300) == false; + assert visit_first(false, 20, false, 20) == false; + assert visit_first(true, 0, true, 0) == false; +} + +test the_round_stands_aside_for_the_reviewer { + assert round_reviews(true) == false; + assert round_reviews(false) == true; +} + +test a_row_that_keeps_waiting_is_visited_less_and_less { + assert revisit_after_seconds(0) == 0; + assert revisit_after_seconds(1) == 30; + assert revisit_after_seconds(2) == 60; + assert revisit_after_seconds(3) == 120; + assert revisit_after_seconds(5) == 480; + assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS; + // a row that never waited is always due; one that waited is due only once its time has passed + assert due_again(0, 0) == true; + assert due_again(1, 29) == false; + assert due_again(1, 30) == true; + assert due_again(6, 899) == false; + assert due_again(6, 900) == true; +} + +test no_bundle_is_empty_for_a_runner_row_past_the_grace { + // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true; + // the six hours the frozen-wait valve would otherwise have held it + assert no_bundle_is_empty(true, false, 360) == true; + assert no_bundle_is_empty(true, false, 4294967295) == true; +} + +test no_bundle_is_empty_waits_inside_the_grace { + // just finished: the runner may not have stored its bundle yet + assert no_bundle_is_empty(true, false, 0) == false; + // a bundle 40 s late is still awaited, and judged on its branch when it arrives + assert no_bundle_is_empty(true, false, 1) == false; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false; +} + +test no_bundle_is_empty_leaves_a_container_row_alone { + // a bee that ran here has no bundle by design; its diff failing decides nothing + assert no_bundle_is_empty(false, false, 0) == false; + assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(false, false, 4294967295) == false; + assert no_bundle_is_empty(false, true, 4294967295) == false; +} + +test no_bundle_is_empty_leaves_a_stored_bundle_alone { + // the bundle is there, so the diff failed for another reason: still a wait + assert no_bundle_is_empty(true, true, 0) == false; + assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(true, true, 4294967295) == false; +} + +invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour { + // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit + assert NO_BUNDLE_GRACE_MINUTES > 1; + assert no_bundle_is_empty(true, false, 1) == false; + // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit + // that judges it comes within the hour of its finish, not after the six-hour valve + assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR; +} + +invariant the_reviewer_is_quicker_than_the_round { + assert REVIEWER_EVERY_SECONDS < 60; + assert REVIEWER_CONCURRENCY > 1; + assert REVIEW_ROW_SECONDS >= 120; + // a stuck row is still seen several times an hour, so one that starts to read is not lost for long + assert WAIT_REVISIT_CAP_SECONDS <= 3600; + assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS; +} + +invariant the_reviewer_can_outrun_the_measured_finish_rate { + // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets + // at least the slots that load needs, and the ceiling is not what stops it + assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58); + assert REVIEWER_CEILING >= slots_needed(58); + assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY; + // the fixed bound alone could not, which is why it is only the fallback + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours + assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0; + // FR-001: 0 only when nothing waits + assert reviewer_concurrency(0, 0, 0, 0, 0) == 0; + assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1; +} diff --git a/apps/website/public/widgets/hive-reviewers/tool.js b/apps/website/public/widgets/hive-reviewers/tool.js new file mode 100644 index 0000000000..42506db75d --- /dev/null +++ b/apps/website/public/widgets/hive-reviewers/tool.js @@ -0,0 +1,33 @@ +// hive-reviewers/tool.js -- lesson 10 of queen-and-hive: how many reviews must run at once. The first +// meter is reviewer_concurrency, the second slots_needed and the gate keeps_up, fn bodies of +// gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm (./logic.js). Words are SAY_ and numbers K_ +// constants of specs/widgets/hive-reviewers.t27. +import { keepsUp, reviewerConcurrency, slotsNeeded } from './logic.js' +import { K, W, ask, controls, el, fill, gate, meters, root, say, source } from '../hive-kit.js' + +const ctl = controls([ + { key: 'keys', label: say('SAY_KEYS'), min: 0, max: K('K_KEYS_MAX', 20), step: 1 }, + { key: 'lanes', label: say('SAY_LANES'), min: 1, max: K('K_LANES_MAX', 4), step: 1 }, + { key: 'mb', label: say('SAY_MB'), min: 0, max: K('K_MB_MAX', 8000), step: K('K_MB_STEP', 250) }, + { key: 'per', label: say('SAY_PER'), min: 0, max: K('K_PER_MAX', 1000), step: K('K_PER_STEP', 50) }, + { key: 'waiting', label: say('SAY_WAITING'), min: 0, max: K('K_WAITING_MAX', 150), step: 1 }, + { key: 'rate', label: say('SAY_RATE'), min: 0, max: K('K_RATE_MAX', 120), step: 1 }, +], { names: W.SAY_PRESET_NAMES, values: { keys: K('K_PRESET_KEYS', []), lanes: K('K_PRESET_LANES', []), mb: K('K_PRESET_MB', []), per: K('K_PRESET_PER', []), waiting: K('K_PRESET_WAITING', []), rate: K('K_PRESET_RATE', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const slots = reviewerConcurrency(s.keys, s.lanes, s.mb, s.per, s.waiting) + const need = slotsNeeded(s.rate) + const ok = keepsUp(slots, s.rate) + const top = K('K_REVIEWER_CEILING', 16), fixed = K('K_REVIEWER_CONCURRENCY', 4) + out.replaceChildren( + meters([ + { label: say('SAY_AT_ONCE'), value: slots, max: top, text: String(slots), tone: ok ? 0 : 6, mark: fixed, markLabel: fill(say('SAY_FIXED'), fixed) }, + { label: say('SAY_NEEDED'), value: need, max: top, text: String(need), tone: 1 }, + ]), + gate(ok, ok ? say('SAY_KEEPS') : say('SAY_BEHIND'), fill(say('SAY_GAP'), slots, need))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-revisit/card.png b/apps/website/public/widgets/hive-revisit/card.png new file mode 100644 index 0000000000..76442a080f Binary files /dev/null and b/apps/website/public/widgets/hive-revisit/card.png differ diff --git a/apps/website/public/widgets/hive-revisit/hive-revisit.t27 b/apps/website/public/widgets/hive-revisit/hive-revisit.t27 new file mode 100644 index 0000000000..d85c6310a9 --- /dev/null +++ b/apps/website/public/widgets/hive-revisit/hive-revisit.t27 @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-revisit.t27 -- Back off: when the reviewer looks at a waiting row again +; Source of truth for public/widgets/hive-revisit/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-revisit/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 11 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the waits in a row and the seconds since the last visit. Each bar +; is revisit_after_seconds of its wait count; the dial and the verdict are due_again(waits, +; seconds). + +; WHAT IT DOES NOT CLAIM: the 941 and 888 are the spec record of 2026-10-09; the page visits +; nothing. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_revisit; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-revisit"; +pub const TITLE : str = "Back off: when the reviewer looks at a waiting row again"; +pub const DESCRIPTION : str = "A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due."; +pub const IMAGE_ALT : str = "Revisit card: a row that answered wait 4 times, 200 s after its last visit. Bars for 0 to 8 waits read 0, 30, 60, 120, 240, 480, 900, 900, 900 seconds, the fifth lit. The clock dial fills to 200 of 240: not due yet."; +pub const HOOK : str = "Measured over 44 minutes: 941 visits, 888 of them wait, from 22 rows. One row was visited 54 times. Doubling the pause gave those slots back."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is revisit_after_seconds and due_again, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 11 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_WAITS_MAX : u8 = 8; +pub const K_SINCE_MAX : u16 = 1000; +pub const K_SINCE_STEP : u8 = 10; +pub const K_WAIT_REVISIT_BASE_SECONDS : u32 = 30; +pub const K_WAIT_REVISIT_CAP_SECONDS : u32 = 900; +pub const K_PRESET_WAITS : [4]u8 = [4, 1, 7, 0]; +pub const K_PRESET_SINCE : [4]u16 = [200, 40, 600, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Four waits, 200 s", "One wait, 40 s", "Seven waits, at the cap", "A fresh row"]; +pub const SAY_ASK : str = "When should the reviewer look again at a row that keeps answering wait?"; +pub const SAY_WAITS : str = "waits in a row"; +pub const SAY_SINCE : str = "seconds since the last visit"; +pub const SAY_BARS : str = "pause before the next visit, by waits in a row"; +pub const SAY_DUE : str = "due again"; +pub const SAY_NOT_DUE : str = "not due yet"; +pub const SAY_PAUSE : str = "pause, s"; +pub const SAY_NOTE : str = "Any answer other than wait resets the count, so a row that starts to read is visited at the normal pace again. The cap keeps a stuck row in sight several times an hour."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/reviewer.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_pause_starts_at_30_and_stops_at_900 { + assert K_WAIT_REVISIT_BASE_SECONDS == 30; + assert K_WAIT_REVISIT_CAP_SECONDS == 900; + assert K_SINCE_MAX > K_WAIT_REVISIT_CAP_SECONDS; +} + +test the_presets_fit { + assert K_PRESET_WAITS[2] <= K_WAITS_MAX; + assert K_PRESET_SINCE[2] <= K_SINCE_MAX; +} diff --git a/apps/website/public/widgets/hive-revisit/index.html b/apps/website/public/widgets/hive-revisit/index.html new file mode 100644 index 0000000000..274fd53db7 --- /dev/null +++ b/apps/website/public/widgets/hive-revisit/index.html @@ -0,0 +1,47 @@ + + + + + + +Back off: when the reviewer looks at a waiting row again · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Back off: when the reviewer looks at a waiting row again

+

A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is revisit_after_seconds and due_again, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 11 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-revisit/logic.js b/apps/website/public/widgets/hive-revisit/logic.js new file mode 100644 index 0000000000..717e5ef45b --- /dev/null +++ b/apps/website/public/widgets/hive-revisit/logic.js @@ -0,0 +1,60 @@ +// Generated by scripts/t27-logic.mjs from reviewer.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 0b69832d9d471757488d65e36935f088efa6d4c976bc04e58ce9d557698a8063 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-revisit/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABJAVgA39/fwF/YAJ/fwF/YAF/AX9gBH9/f38Bf2AFf39/f38BfwMNDAABAgICAwEAAQICBAQFAXABAQEFAwEAEAYJAX8BQYCAwAALB98BCwZtZW1vcnkCABZ0Mjdfbm9fYnVuZGxlX2lzX2VtcHR5AAANdDI3X2R1ZV9hZ2FpbgABGXQyN19yZXZpc2l0X2FmdGVyX3NlY29uZHMAAxF0Mjdfcm91bmRfcmV2aWV3cwAED3QyN192aXNpdF9maXJzdAAFEHQyN19yZXZpZXdfc2xvdHMABhd0MjdfcmV2aWV3X3Nsb3RzX3dpdGhpbgAHDHQyN19rZWVwc191cAAIEHQyN19zbG90c19uZWVkZWQAChh0MjdfcmV2aWV3ZXJfY29uY3VycmVuY3kACwqoAwwiAQF/QQAhAwJAIABBAXFFDQAgAUF/cyACQQlLcSEDCyADCw0AIAEgABCCgICAAE8LTgECfwJAIAANAEEADwtBHiEBQQEhAgN/AkACQCAAIAJGDQAgAkEGRw0BQYQHDwsgAUGEByABQYQHSRsPCyACQQFqIQIgAUEBdCEBDAALCwoAIAAQgoCAgAALBwAgAEEBcwswAQF/QQEhBAJAAkAgAEEBcUUNACACQQFxRQ0BCyAAIAJBf3NyIAEgA0txIQQLIAQLGwEBfyABQQQgAGsiAiABIAJJG0EAIABBBEkbCxsBAX8gAiAAIAFrIgMgAiADSRtBACAAIAFLGwsNACAAIAEQiYCAgABPCzIBAX8gAEGQHG4iAUGsAmwgACABQZAcbGsiAEH//wNxQQxuaiAAQawCbEGQHHBBAEdqCwoAIAAQiYCAgAALXgACQCAEDQBBAA8LIAFBECABQRBJGyAAQRAgAEEQSRtsIQBBASEBAkAgA0UNACACIANuIQELIAAgASAAIAFJGyIBIAQgASAESRsiBEEBIARBAUsbIgRBECAEQRBJGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function reviewerConcurrency(keys, lanesPerKey, freeMb, mbPerReview, waiting) { + return W.t27_reviewer_concurrency(int(keys, 0, 4294967295, "reviewer_concurrency.keys"), int(lanesPerKey, 0, 4294967295, "reviewer_concurrency.lanes_per_key"), int(freeMb, 0, 4294967295, "reviewer_concurrency.free_mb"), int(mbPerReview, 0, 4294967295, "reviewer_concurrency.mb_per_review"), int(waiting, 0, 4294967295, "reviewer_concurrency.waiting")) >>> 0; +} +function slotsNeeded(finishedPerHour) { + return W.t27_slots_needed(int(finishedPerHour, 0, 4294967295, "slots_needed.finished_per_hour")) >>> 0; +} +function keepsUp(slots, finishedPerHour) { + return (W.t27_keeps_up(int(slots, 0, 4294967295, "keeps_up.slots"), int(finishedPerHour, 0, 4294967295, "keeps_up.finished_per_hour")) & 1) !== 0; +} +function reviewSlotsWithin(bound, inFlight, waiting) { + return W.t27_review_slots_within(int(bound, 0, 4294967295, "review_slots_within.bound"), int(inFlight, 0, 4294967295, "review_slots_within.in_flight"), int(waiting, 0, 4294967295, "review_slots_within.waiting")) >>> 0; +} +function reviewSlots(inFlight, waiting) { + return W.t27_review_slots(int(inFlight, 0, 4294967295, "review_slots.in_flight"), int(waiting, 0, 4294967295, "review_slots.waiting")) >>> 0; +} +function visitFirst(neverA, secondsSinceA, neverB, secondsSinceB) { + return (W.t27_visit_first(neverA ? 1 : 0, int(secondsSinceA, 0, 4294967295, "visit_first.seconds_since_a"), neverB ? 1 : 0, int(secondsSinceB, 0, 4294967295, "visit_first.seconds_since_b")) & 1) !== 0; +} +function roundReviews(reviewerRunning) { + return (W.t27_round_reviews(reviewerRunning ? 1 : 0) & 1) !== 0; +} +function revisitAfterSeconds(waits) { + return W.t27_revisit_after_seconds(int(waits, 0, 4294967295, "revisit_after_seconds.waits")) >>> 0; +} +function dueAgain(waits, secondsSinceVisit) { + return (W.t27_due_again(int(waits, 0, 4294967295, "due_again.waits"), int(secondsSinceVisit, 0, 4294967295, "due_again.seconds_since_visit")) & 1) !== 0; +} +function noBundleIsEmpty(onRunner, bundleStored, minutesSinceFinished) { + return (W.t27_no_bundle_is_empty(onRunner ? 1 : 0, bundleStored ? 1 : 0, int(minutesSinceFinished, 0, 4294967295, "no_bundle_is_empty.minutes_since_finished")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + dueAgain, + keepsUp, + noBundleIsEmpty, + reviewSlots, + reviewSlotsWithin, + reviewerConcurrency, + revisitAfterSeconds, + roundReviews, + slotsNeeded, + visitFirst +}; diff --git a/apps/website/public/widgets/hive-revisit/logic.t27 b/apps/website/public/widgets/hive-revisit/logic.t27 new file mode 100644 index 0000000000..dcc617112b --- /dev/null +++ b/apps/website/public/widgets/hive-revisit/logic.t27 @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/reviewer.t27 -- the Queen's reviewer runs on its own clock, several reviews at a time +; Owner, 2026-10-08 (translated): "do point 3 -- a separate reviewer". Measured that afternoon on the +; live Queen (gHashTag/BrowserOS trios/agent-server): +; - bees finished 47 tasks in an hour and the Queen reviewed 23, so finished work queued up (91); +; - the review sweep ran INSIDE the round, before it handed out work: at most 3 reviews, one after +; another, and at most 45 s of wall clock (0.75 of a 60 s tick), each review a model call of up to +; 2 minutes -- in practice about one review a round; +; - every finished task waiting for its verdict held its files (holds.t27 now lets go after 30 min). +; The reviewer moves out of the round: its own clock, up to REVIEWER_CONCURRENCY rows at once, each +; on its own lane, one row never twice at once, and every waiting row visited in turn. The round +; then reviews nothing: two sweeps over one row would race on its verdict. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenReviewer; + +/// How often the reviewer looks for rows to take: often, because a pass that finds every slot busy +/// costs one query. +pub const REVIEWER_EVERY_SECONDS : u32 = 10; + +/// Rows reviewed at once by a host that has not measured its lanes and memory: the fixed bound the +/// reviewer started with. Each row takes its own lane (a key no bee and no other review holds). +/// A host that knows its keys, lanes and free memory uses `reviewer_concurrency` instead; this stays +/// as the bound behind `review_slots` so callers that pass no measurement keep their behaviour. +pub const REVIEWER_CONCURRENCY : u32 = 4; + +/// The most rows reviewed at once, whatever the lanes and memory allow: four times the fixed 4, and +/// three times the 5 the measured load of 2026-10-09 needs (see `slots_needed`). +pub const REVIEWER_CEILING : u32 = 16; + +/// Seconds in an hour; finish rates are measured per hour. +pub const SECONDS_PER_HOUR : u32 = 3600; + +/// One row's wall clock: the witness, the criteria measurement and the model call. It no longer +/// shares a 45 s budget with the round, because it no longer holds the round up. +pub const REVIEW_ROW_SECONDS : u32 = 300; + +/// Measured 2026-10-09 on the live Queen (`/queen/status`, `/queen/public-activity`): about 58 +/// dispatches finished an hour against about 11 reviewed, with 111 rows waiting. At REVIEW_ROW_SECONDS +/// a slot reviews at most 3600 / 300 = 12 rows an hour, so the fixed 4 tops out at 48 an hour: below +/// the finish rate even if every review used its whole budget, and the backlog only grows. +/// How many rows the reviewer may take at once, from what the host measures: +/// - `keys` model keys, each with `lanes_per_key` lanes free for review; +/// - `free_mb` megabytes free, each review costing `mb_per_review` (0 = not measured: one review); +/// - never above REVIEWER_CEILING, never above the rows `waiting`; +/// - 0 only when nothing waits, and at least 1 while anything waits, so the backlog always drains. +pub fn reviewer_concurrency(keys: u32, lanes_per_key: u32, free_mb: u32, mb_per_review: u32, waiting: u32) -> u32 { + if (waiting == 0) { return 0; } + var k : u32 = keys; + if (k > REVIEWER_CEILING) { k = REVIEWER_CEILING; } + var l : u32 = lanes_per_key; + if (l > REVIEWER_CEILING) { l = REVIEWER_CEILING; } + var bound : u32 = k * l; + if (bound > REVIEWER_CEILING) { bound = REVIEWER_CEILING; } + var by_memory : u32 = 1; + if (mb_per_review > 0) { by_memory = free_mb / mb_per_review; } + if (by_memory < bound) { bound = by_memory; } + if (waiting < bound) { bound = waiting; } + if (bound < 1) { return 1; } + return bound; +} + +/// Rows at once needed so the reviewer finishes at least `finished_per_hour` rows an hour when every +/// row uses its whole REVIEW_ROW_SECONDS: ceil(finished_per_hour * REVIEW_ROW_SECONDS / 3600). +/// Split by the hour so no u32 product overflows. +pub fn slots_needed(finished_per_hour: u32) -> u32 { + let whole : u32 = finished_per_hour / SECONDS_PER_HOUR; + let part : u32 = finished_per_hour % SECONDS_PER_HOUR; + let part_seconds : u32 = part * REVIEW_ROW_SECONDS; + var extra : u32 = part_seconds / SECONDS_PER_HOUR; + if (part_seconds % SECONDS_PER_HOUR > 0) { extra = extra + 1; } + return whole * REVIEW_ROW_SECONDS + extra; +} + +/// Whether `slots` reviews at once keep up with `finished_per_hour` finished rows. +pub fn keeps_up(slots: u32, finished_per_hour: u32) -> bool { + return slots >= slots_needed(finished_per_hour); +} + +/// How many rows a pass starts against a bound (normally `reviewer_concurrency`): the free slots, but +/// no more than the rows waiting. +pub fn review_slots_within(bound: u32, in_flight: u32, waiting: u32) -> u32 { + if (in_flight >= bound) { return 0; } + let room : u32 = bound - in_flight; + if (waiting < room) { return waiting; } + return room; +} + +/// How many rows a pass starts on a host that measured nothing: the same rule against the fixed +/// REVIEWER_CONCURRENCY, unchanged for its existing callers. +pub fn review_slots(in_flight: u32, waiting: u32) -> u32 { + return review_slots_within(REVIEWER_CONCURRENCY, in_flight, waiting); +} + +/// The order rows are taken in: the one this reviewer visited longest ago first, a row it never +/// visited before any it has. Without it, a row that is re-read and waits again (no review spent) +/// keeps its place at the head and the reviewer circles the same few rows. +pub fn visit_first(never_a: bool, seconds_since_a: u32, never_b: bool, seconds_since_b: u32) -> bool { + if (never_a and never_b == false) { return true; } + if (never_b and never_a == false) { return false; } + return seconds_since_a > seconds_since_b; +} + +/// While the reviewer runs, the round reviews nothing. +pub fn round_reviews(reviewer_running: bool) -> bool { + return reviewer_running == false; +} + +/// A row that answered `wait` is not visited again at once. Measured 2026-10-09 on the live Queen, +/// over 44 minutes: 941 row visits, 888 of them `wait`, from just 22 rows whose branch the Queen could +/// not read. One row was visited 54 times. Only 53 visits decided anything. Nothing about such a row +/// changes in 50 s, and the frozen-wait valve releases it only after six hours, so each one burned +/// about 430 visits of the reviewer's slots. +/// After n waits in a row, the next visit waits WAIT_REVISIT_BASE_SECONDS doubled n - 1 times, never +/// more than WAIT_REVISIT_CAP_SECONDS. Any answer other than `wait` resets the count, so a row that +/// has started to read is visited at the normal pace again. +pub const WAIT_REVISIT_BASE_SECONDS : u32 = 30; +pub const WAIT_REVISIT_CAP_SECONDS : u32 = 900; + +pub fn revisit_after_seconds(waits: u32) -> u32 { + if (waits == 0) { return 0; } + var wait : u32 = WAIT_REVISIT_BASE_SECONDS; + var n : u32 = 1; + while (n < waits) { + if (wait >= WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + wait = wait * 2; + n = n + 1; + } + if (wait > WAIT_REVISIT_CAP_SECONDS) { return WAIT_REVISIT_CAP_SECONDS; } + return wait; +} + +/// Whether a row may be taken now: one that never waited always may, and one that waited only after +/// its revisit time has passed since the last visit. +pub fn due_again(waits: u32, seconds_since_visit: u32) -> bool { + return seconds_since_visit >= revisit_after_seconds(waits); +} + +/// A RUNNER BEE THAT COMMITTED NOTHING LEAVES NOTHING TO READ (gHashTag/t27#9069). Measured on the +/// live Queen, 07:10-09:43Z on 2026-10-11: 425 of the reviewer's 480 row visits answered `wait` +/// because the diff of queen-N could not be read. A runner stores its bee's branch as a bundle only +/// when the bee committed something, so a bee that committed nothing leaves no branch here, the +/// diff fails, and the row waits for the six-hour frozen-wait valve while its issue stays claimed. +/// A container bee that commits nothing is judged an empty attempt at once, and its issue is free +/// again after the retry floor. +/// A runner stores the bundle after the turn has ended, so a row may be visited before its bundle +/// is there. Over 07:10-10:31Z, 15 runner rows whose diff failed got their bundle on a later visit, +/// the latest 58 s after the first. So a finished runner row with no stored bundle is an empty +/// attempt only once NO_BUNDLE_GRACE_MINUTES have passed since it finished; before that it waits, +/// and a bundle that arrives meanwhile is judged on its branch. A container row (`on_runner` +/// false) and a row whose bundle is stored are never decided here: their diff failed for some +/// other reason, and that still decides nothing. +pub const NO_BUNDLE_GRACE_MINUTES : u32 = 10; + +pub fn no_bundle_is_empty(on_runner: bool, bundle_stored: bool, minutes_since_finished: u32) -> bool { + if (on_runner == false) { return false; } + if (bundle_stored) { return false; } + return minutes_since_finished >= NO_BUNDLE_GRACE_MINUTES; +} + +test a_pass_fills_the_free_slots_with_waiting_rows { + assert review_slots(0, 0) == 0; + assert review_slots(0, 2) == 2; + assert review_slots(0, 100) == REVIEWER_CONCURRENCY; + assert review_slots(1, 100) == REVIEWER_CONCURRENCY - 1; + assert review_slots(REVIEWER_CONCURRENCY - 1, 1) == 1; + assert review_slots(REVIEWER_CONCURRENCY, 100) == 0; + assert review_slots(REVIEWER_CONCURRENCY + 1, 100) == 0; +} + +test the_bound_follows_lanes_memory_and_backlog { + // nothing waits: no slot, whatever the host has + assert reviewer_concurrency(10, 10, 100000, 100, 0) == 0; + // lanes bound: 2 keys x 3 lanes + assert reviewer_concurrency(2, 3, 100000, 100, 111) == 6; + // memory bound: 1000 MB at 300 MB a review + assert reviewer_concurrency(10, 10, 1000, 300, 111) == 3; + // the hard ceiling: 16, though 10 x 10 lanes and 1000 reviews' worth of memory are free + assert reviewer_concurrency(10, 10, 100000, 100, 111) == 16; + assert reviewer_concurrency(4294967295, 4294967295, 4294967295, 1, 4294967295) == REVIEWER_CEILING; + // never above the backlog + assert reviewer_concurrency(10, 10, 100000, 100, 2) == 2; + // at least one while anything waits, even with no lane or memory measured + assert reviewer_concurrency(0, 0, 0, 0, 1) == 1; + assert reviewer_concurrency(10, 10, 100, 300, 111) == 1; + assert reviewer_concurrency(10, 10, 100000, 0, 111) == 1; + assert reviewer_concurrency(10, 0, 100000, 100, 111) == 1; +} + +test the_measured_load_needs_five_and_the_bound_admits_them { + // 58 finished an hour at 300 s a row: ceil(58 * 300 / 3600) = ceil(4.83) = 5 + assert slots_needed(58) == 5; + assert slots_needed(0) == 0; + assert slots_needed(12) == 1; + assert slots_needed(13) == 2; + assert slots_needed(3600) == REVIEW_ROW_SECONDS; + assert slots_needed(3601) == REVIEW_ROW_SECONDS + 1; + assert slots_needed(4294967295) == 357913942; + // the fixed 4 cannot keep up; 70 lanes (measured 2026-10-09) and 8 GB at 512 MB a review can + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + assert keeps_up(reviewer_concurrency(7, 10, 8192, 512, 111), 58) == true; + assert reviewer_concurrency(7, 10, 8192, 512, 111) == 16; + assert keeps_up(4, 48) == true; + assert keeps_up(4, 49) == false; +} + +test a_pass_fills_the_free_slots_against_the_computed_bound { + let bound : u32 = reviewer_concurrency(7, 10, 8192, 512, 111); + assert review_slots_within(bound, 0, 111) == 16; + assert review_slots_within(bound, 11, 111) == 5; + assert review_slots_within(bound, 15, 3) == 1; + assert review_slots_within(bound, 16, 111) == 0; + assert review_slots_within(bound, 17, 111) == 0; + assert review_slots_within(bound, 0, 3) == 3; + assert review_slots_within(0, 0, 111) == 0; +} + +test every_row_is_visited_in_turn { + // never visited goes first + assert visit_first(true, 0, false, 9999) == true; + assert visit_first(false, 9999, true, 0) == false; + // otherwise the one visited longest ago + assert visit_first(false, 300, false, 20) == true; + assert visit_first(false, 20, false, 300) == false; + assert visit_first(false, 20, false, 20) == false; + assert visit_first(true, 0, true, 0) == false; +} + +test the_round_stands_aside_for_the_reviewer { + assert round_reviews(true) == false; + assert round_reviews(false) == true; +} + +test a_row_that_keeps_waiting_is_visited_less_and_less { + assert revisit_after_seconds(0) == 0; + assert revisit_after_seconds(1) == 30; + assert revisit_after_seconds(2) == 60; + assert revisit_after_seconds(3) == 120; + assert revisit_after_seconds(5) == 480; + assert revisit_after_seconds(6) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(50) == WAIT_REVISIT_CAP_SECONDS; + assert revisit_after_seconds(4294967295) == WAIT_REVISIT_CAP_SECONDS; + // a row that never waited is always due; one that waited is due only once its time has passed + assert due_again(0, 0) == true; + assert due_again(1, 29) == false; + assert due_again(1, 30) == true; + assert due_again(6, 899) == false; + assert due_again(6, 900) == true; +} + +test no_bundle_is_empty_for_a_runner_row_past_the_grace { + // a runner row with no stored bundle, the grace spent: an empty attempt, not a wait + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES) == true; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES + 1) == true; + // the six hours the frozen-wait valve would otherwise have held it + assert no_bundle_is_empty(true, false, 360) == true; + assert no_bundle_is_empty(true, false, 4294967295) == true; +} + +test no_bundle_is_empty_waits_inside_the_grace { + // just finished: the runner may not have stored its bundle yet + assert no_bundle_is_empty(true, false, 0) == false; + // a bundle 40 s late is still awaited, and judged on its branch when it arrives + assert no_bundle_is_empty(true, false, 1) == false; + assert no_bundle_is_empty(true, false, NO_BUNDLE_GRACE_MINUTES - 1) == false; +} + +test no_bundle_is_empty_leaves_a_container_row_alone { + // a bee that ran here has no bundle by design; its diff failing decides nothing + assert no_bundle_is_empty(false, false, 0) == false; + assert no_bundle_is_empty(false, false, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(false, false, 4294967295) == false; + assert no_bundle_is_empty(false, true, 4294967295) == false; +} + +test no_bundle_is_empty_leaves_a_stored_bundle_alone { + // the bundle is there, so the diff failed for another reason: still a wait + assert no_bundle_is_empty(true, true, 0) == false; + assert no_bundle_is_empty(true, true, NO_BUNDLE_GRACE_MINUTES) == false; + assert no_bundle_is_empty(true, true, 4294967295) == false; +} + +invariant a_runner_row_with_no_bundle_is_judged_inside_the_hour { + // the latest late bundle measured on 2026-10-11 came 58 s after the row's first visit + assert NO_BUNDLE_GRACE_MINUTES > 1; + assert no_bundle_is_empty(true, false, 1) == false; + // a row that keeps waiting is visited at least every WAIT_REVISIT_CAP_SECONDS, so the visit + // that judges it comes within the hour of its finish, not after the six-hour valve + assert NO_BUNDLE_GRACE_MINUTES * 60 + WAIT_REVISIT_CAP_SECONDS <= SECONDS_PER_HOUR; +} + +invariant the_reviewer_is_quicker_than_the_round { + assert REVIEWER_EVERY_SECONDS < 60; + assert REVIEWER_CONCURRENCY > 1; + assert REVIEW_ROW_SECONDS >= 120; + // a stuck row is still seen several times an hour, so one that starts to read is not lost for long + assert WAIT_REVISIT_CAP_SECONDS <= 3600; + assert WAIT_REVISIT_BASE_SECONDS >= REVIEWER_EVERY_SECONDS; +} + +invariant the_reviewer_can_outrun_the_measured_finish_rate { + // FR-002: at 58 finished an hour and REVIEW_ROW_SECONDS, a host with the lanes and memory gets + // at least the slots that load needs, and the ceiling is not what stops it + assert keeps_up(reviewer_concurrency(REVIEWER_CEILING, REVIEWER_CEILING, 4294967295, 1, 111), 58); + assert REVIEWER_CEILING >= slots_needed(58); + assert REVIEWER_CEILING >= REVIEWER_CONCURRENCY; + // the fixed bound alone could not, which is why it is only the fallback + assert keeps_up(REVIEWER_CONCURRENCY, 58) == false; + // one hour holds a whole number of review budgets, so slots_needed is exact at whole hours + assert SECONDS_PER_HOUR % REVIEW_ROW_SECONDS == 0; + // FR-001: 0 only when nothing waits + assert reviewer_concurrency(0, 0, 0, 0, 0) == 0; + assert reviewer_concurrency(0, 0, 0, 0, 1) >= 1; +} diff --git a/apps/website/public/widgets/hive-revisit/tool.js b/apps/website/public/widgets/hive-revisit/tool.js new file mode 100644 index 0000000000..55f1894d5c --- /dev/null +++ b/apps/website/public/widgets/hive-revisit/tool.js @@ -0,0 +1,28 @@ +// hive-revisit/tool.js -- lesson 11 of queen-and-hive: when the reviewer looks again at a row that +// answered wait. Every bar is revisit_after_seconds of its wait count, and the dial and verdict are +// due_again, fn bodies of gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm (./logic.js). Words +// are SAY_ and numbers K_ constants of specs/widgets/hive-revisit.t27. +import { dueAgain, revisitAfterSeconds } from './logic.js' +import { K, W, ask, controls, dial, el, head, meters, root, say, source, two, verdict } from '../hive-kit.js' + +const WMAX = K('K_WAITS_MAX', 8) +const ctl = controls([ + { key: 'waits', label: say('SAY_WAITS'), min: 0, max: WMAX, step: 1 }, + { key: 'since', label: say('SAY_SINCE'), min: 0, max: K('K_SINCE_MAX', 1000), step: K('K_SINCE_STEP', 10) }, +], { names: W.SAY_PRESET_NAMES, values: { waits: K('K_PRESET_WAITS', []), since: K('K_PRESET_SINCE', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const cap = K('K_WAIT_REVISIT_CAP_SECONDS', 900) + const pause = revisitAfterSeconds(s.waits) + const due = dueAgain(s.waits, s.since) + out.replaceChildren( + two( + el('div', {}, head(say('SAY_BARS')), meters([...Array(WMAX + 1)].map((_, n) => { const v = revisitAfterSeconds(n); return { label: `${n}`, value: v, max: cap, text: `${v} s`, tone: n === s.waits ? 1 : 2 } }))), + el('div', {}, dial({ value: Math.min(s.since, pause || 1), max: pause || 1, tone: due ? 0 : 1, big: `${s.since} s`, small: `${say('SAY_PAUSE')} ${pause}`, label: say('SAY_PAUSE') }), + due ? verdict(say('SAY_DUE'), 0) : verdict(say('SAY_NOT_DUE'), 1)))) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-shape/card.png b/apps/website/public/widgets/hive-shape/card.png new file mode 100644 index 0000000000..2c8497ee4b Binary files /dev/null and b/apps/website/public/widgets/hive-shape/card.png differ diff --git a/apps/website/public/widgets/hive-shape/hive-shape.t27 b/apps/website/public/widgets/hive-shape/hive-shape.t27 new file mode 100644 index 0000000000..64800a668d --- /dev/null +++ b/apps/website/public/widgets/hive-shape/hive-shape.t27 @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-shape.t27 -- Shaping: what the Queen does with an issue that is not ready +; Source of truth for public/widgets/hive-shape/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-shape/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/shaping.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 1 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the sections judge() finds missing, whether the issue is an epic, +; the attempts so far, whether the author was told, and the minutes since the last attempt. The card +; goes to the column shape_action names; the row of attempts 0 to 4 is shape_action at each count; +; the tile is attempt_due. + +; WHAT IT DOES NOT CLAIM: it does not run the model that writes the sections, nor judge() itself +; (specs/policy/issue_shape.t27). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_shape; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-shape"; +pub const TITLE : str = "Shaping: what the Queen does with an issue that is not ready"; +pub const DESCRIPTION : str = "Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing."; +pub const IMAGE_ALT : str = "Shaping card: an issue missing two sections after two failed attempts. The board puts its card in the gold TELL column; the attempt row reads shape, shape, tell, tell, tell for attempts 0 to 4. Tiles read action TELL, attempt due yes after 420 minutes, limit 2."; +pub const HOOK : str = "Two failed shapes and the Queen stops rewriting the issue: she comments once with what is missing, and never again."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is shape_action and attempt_due, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 1 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MISSING_MAX : u8 = 3; +pub const K_ATTEMPTS_MAX : u8 = 4; +pub const K_MINUTES_MAX : u16 = 720; +pub const K_MINUTES_STEP : u8 = 30; +pub const K_SHAPE_ATTEMPT_LIMIT : u32 = 2; +pub const K_SHAPE_RETRY_MINUTES : u32 = 360; +pub const K_PRESET_MISSING : [5]u8 = [2, 0, 2, 1, 2]; +pub const K_PRESET_CONTAINER : [5]u8 = [0, 0, 1, 0, 0]; +pub const K_PRESET_ATTEMPTS : [5]u8 = [2, 0, 0, 0, 3]; +pub const K_PRESET_TOLD : [5]u8 = [0, 0, 0, 0, 1]; +pub const K_PRESET_MINUTES : [5]u16 = [420, 0, 0, 0, 600]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Two shapes failed", "A ready issue", "An epic", "A first try", "Already told"]; +pub const SAY_ASK : str = "What does the Queen do with an issue that is not ready for a bee?"; +pub const SAY_MISSING : str = "sections missing"; +pub const SAY_CONTAINER : str = "an epic or roadmap"; +pub const SAY_ATTEMPTS : str = "shapes tried"; +pub const SAY_TOLD : str = "author already told"; +pub const SAY_MINUTES : str = "minutes since the last try"; +pub const SAY_ACTIONS : [3]str = ["LEAVE", "SHAPE", "TELL"]; +pub const SAY_ACTION_LINES : [3]str = ["nothing to do", "write the missing sections, judge them", "comment once with what is missing"]; +pub const SAY_ISSUE : str = "the issue"; +pub const SAY_ROW : str = "the same issue after 0, 1, 2, 3 and 4 tries"; +pub const SAY_ACTION : str = "action"; +pub const SAY_DUE : str = "another try due"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_LIMIT : str = "attempt limit"; +pub const SAY_NOTE : str = "A shape is one model call, and a refused shape is not retried for 360 minutes. After the limit the Queen stops writing and comments once, so a broken issue never costs a model call every round."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/shaping.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_presets_stay_in_range { + assert K_PRESET_MISSING[0] <= K_MISSING_MAX; + assert K_PRESET_ATTEMPTS[4] <= K_ATTEMPTS_MAX; + assert K_PRESET_MINUTES[4] <= K_MINUTES_MAX; +} + +test the_start_is_past_the_limit { + assert K_PRESET_ATTEMPTS[0] == K_SHAPE_ATTEMPT_LIMIT; + assert K_PRESET_MISSING[0] > 0; + assert K_PRESET_TOLD[0] == 0; +} diff --git a/apps/website/public/widgets/hive-shape/index.html b/apps/website/public/widgets/hive-shape/index.html new file mode 100644 index 0000000000..48aac331bf --- /dev/null +++ b/apps/website/public/widgets/hive-shape/index.html @@ -0,0 +1,47 @@ + + + + + + +Shaping: what the Queen does with an issue that is not ready · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

Shaping: what the Queen does with an issue that is not ready

+

Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is shape_action and attempt_due, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 1 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-shape/logic.js b/apps/website/public/widgets/hive-shape/logic.js new file mode 100644 index 0000000000..1cba116846 --- /dev/null +++ b/apps/website/public/widgets/hive-shape/logic.js @@ -0,0 +1,36 @@ +// Generated by scripts/t27-logic.mjs from shaping.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: e887aacb9648b6efe25fd8fdd334e671e099692647f1d6836d121fe082205b05 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-shape/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABFgNgAn9/AX9gA39/fwF/YAR/f39/AX8DBQQAAQICBAUBcAEBAQUDAQAQBgkBfwFBgIDAAAsHVQUGbWVtb3J5AgAPdDI3X2F0dGVtcHRfZHVlAAAPdDI3X3NoYXBlX2xhbmRzAAERdDI3X3BhdGhfYWNjZXB0ZWQAAhB0Mjdfc2hhcGVfYWN0aW9uAAMKZAQMACAARSABQecCS3ILEQAgACACckUgAUF/akEMSXELIQEBf0EBIQQCQCAAQQFxDQAgASADQQFzcSACcSEECyAECyEAQQBBAUEAQQIgA0EBcRsgAkECSRsgAUEBcRtBACAAGws="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function shapeAction(missing, container, attempts, told) { + return W.t27_shape_action(int(missing, 0, 255, "shape_action.missing"), container ? 1 : 0, int(attempts, 0, 4294967295, "shape_action.attempts"), told ? 1 : 0) & 255; +} +function pathAccepted(exists, dirExists, cited, foreignNew) { + return (W.t27_path_accepted(exists ? 1 : 0, dirExists ? 1 : 0, cited ? 1 : 0, foreignNew ? 1 : 0) & 1) !== 0; +} +function shapeLands(missingAfter, paths, rejected) { + return (W.t27_shape_lands(int(missingAfter, 0, 255, "shape_lands.missing_after"), int(paths, 0, 4294967295, "shape_lands.paths"), int(rejected, 0, 4294967295, "shape_lands.rejected")) & 1) !== 0; +} +function attemptDue(attempts, minutesSinceLast) { + return (W.t27_attempt_due(int(attempts, 0, 4294967295, "attempt_due.attempts"), int(minutesSinceLast, 0, 4294967295, "attempt_due.minutes_since_last")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + attemptDue, + pathAccepted, + shapeAction, + shapeLands +}; diff --git a/apps/website/public/widgets/hive-shape/logic.t27 b/apps/website/public/widgets/hive-shape/logic.t27 new file mode 100644 index 0000000000..e2bf702451 --- /dev/null +++ b/apps/website/public/widgets/hive-shape/logic.t27 @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/shaping.t27 -- the Queen shapes an issue that is not ready, or says once what it lacks +; Owner, 2026-10-08 (translated): forbid issues that break the rules; find every fragile place and fix +; it; 233 + 115 issues are not ready for work because their spec is missing or incomplete. +; Measured that morning on /queen/status: of 704 candidates, the round said "nothing to choose" in most +; rounds while 63 of 70 lanes stood idle. 233 issues were skipped as missingBoundary and 115 as +; incompleteSpec. Some of them have a `## Boundary` heading with prose under it and no path (#7703, +; #7642), which the rule counts as missing. +; -- +; The rule an issue must meet is specs/policy/issue_shape.t27 (judge: one bit per missing section). +; A workflow cannot hold that line here: .yml is owner-only, and `gh issue create` never sees a +; template. So the Queen holds it in her own round. Every open issue that judge() refuses is either +; SHAPED by her (the missing sections are written by a model, every boundary path is checked against +; the repository, and the body is written back only if judge() then returns 0), or, when shaping +; failed SHAPE_ATTEMPT_LIMIT times, its author is told ONCE what is missing. +; -- +; A wrong boundary is worse than a missing one (tools/queen/propose_boundary.py): it reserves files the +; work does not own. So path_accepted refuses a path the model invented: a path that does not exist is +; accepted only when the issue itself cites it and its directory exists, and never when it would be a +; new file in a language the only-t27 rule denies. +; Containers (labels epic, roadmap) are not tasks, as issue_shape says; they are never shaped. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenShaping; + +/// What the Queen does with one open issue. +/// LEAVE nothing: it is ready, a container, or already told +/// SHAPE ask for the missing sections, judge them, write them back if they pass +/// TELL comment once, naming what is missing +pub const SH_LEAVE : u8 = 0; +pub const SH_SHAPE : u8 = 1; +pub const SH_TELL : u8 = 2; + +/// Attempts at shaping one issue before its author is told instead. +pub const SHAPE_ATTEMPT_LIMIT : u32 = 2; +/// Issues shaped per round. A shape is one model call; the round has other work. +pub const SHAPES_PER_ROUND : u32 = 3; +/// Minutes between two attempts on one issue, so a refused shape is not retried every round. +pub const SHAPE_RETRY_MINUTES : u32 = 360; +/// A boundary is a reservation; one naming more files than this is not a task, it is a project. +pub const MAX_BOUNDARY_PATHS : u32 = 12; + +/// `missing`: judge()'s bits for the body (0 = ready); `container`: labelled epic or roadmap; +/// `attempts`: shapes tried so far; `told`: the author was already told. +pub fn shape_action(missing: u8, container: bool, attempts: u32, told: bool) -> u8 { + if (missing == 0) { return SH_LEAVE; } + if (container) { return SH_LEAVE; } + if (attempts < SHAPE_ATTEMPT_LIMIT) { return SH_SHAPE; } + if (told) { return SH_LEAVE; } + return SH_TELL; +} + +/// Is one proposed boundary path acceptable? +/// exists the file is in the repository at the base ref +/// dir_exists its directory is (a new file beside its siblings) +/// cited the issue's own title or body names this path +/// foreign_new it would be a NEW file whose extension the only-t27 rule denies to new work +/// A file that exists is always acceptable: finding the file a title describes is the point. A file +/// that does not exist must be cited by the issue, in a directory that exists, and not foreign. +pub fn path_accepted(exists: bool, dir_exists: bool, cited: bool, foreign_new: bool) -> bool { + if (exists) { return true; } + if (dir_exists == false) { return false; } + if (foreign_new) { return false; } + return cited; +} + +/// Does a shape land? Only when the body it produces is ready (judge() = 0), it names between 1 and +/// MAX_BOUNDARY_PATHS paths, and every one of them was accepted. +pub fn shape_lands(missing_after: u8, paths: u32, rejected: u32) -> bool { + if (missing_after != 0) { return false; } + if (paths == 0) { return false; } + if (paths > MAX_BOUNDARY_PATHS) { return false; } + return rejected == 0; +} + +/// Is an attempt due? Never twice inside SHAPE_RETRY_MINUTES on one issue. +pub fn attempt_due(attempts: u32, minutes_since_last: u32) -> bool { + if (attempts == 0) { return true; } + return minutes_since_last >= SHAPE_RETRY_MINUTES; +} + +test a_ready_issue_and_a_container_are_left_alone { + assert shape_action(0, false, 0, false) == SH_LEAVE; + assert shape_action(15, true, 0, false) == SH_LEAVE; + assert shape_action(1, true, 9, false) == SH_LEAVE; +} + +test an_unready_issue_is_shaped_then_its_author_is_told_once { + assert shape_action(1, false, 0, false) == SH_SHAPE; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT - 1, false) == SH_SHAPE; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, false) == SH_TELL; + assert shape_action(15, false, SHAPE_ATTEMPT_LIMIT, true) == SH_LEAVE; + assert shape_action(8, false, SHAPE_ATTEMPT_LIMIT + 3, true) == SH_LEAVE; +} + +test a_boundary_path_must_be_true_of_the_repository { + // an existing file, cited or found + assert path_accepted(true, true, false, false) == true; + assert path_accepted(true, true, true, false) == true; + // a new spec the issue asks for, beside its siblings + assert path_accepted(false, true, true, false) == true; + // a new file the issue never mentioned: invented + assert path_accepted(false, true, false, false) == false; + // a new file in a directory that does not exist + assert path_accepted(false, false, true, false) == false; + // a new .py or .ts the only-t27 rule denies, even when cited + assert path_accepted(false, true, true, true) == false; +} + +test a_shape_lands_only_when_the_body_is_ready_and_every_path_holds { + assert shape_lands(0, 1, 0) == true; + assert shape_lands(0, MAX_BOUNDARY_PATHS, 0) == true; + assert shape_lands(1, 3, 0) == false; + assert shape_lands(0, 0, 0) == false; + assert shape_lands(0, MAX_BOUNDARY_PATHS + 1, 0) == false; + assert shape_lands(0, 3, 1) == false; +} + +test a_refused_shape_is_not_retried_every_round { + assert attempt_due(0, 0) == true; + assert attempt_due(1, SHAPE_RETRY_MINUTES - 1) == false; + assert attempt_due(1, SHAPE_RETRY_MINUTES) == true; +} + +invariant the_codes_and_limits_never_move { + assert SH_LEAVE == 0 and SH_SHAPE == 1 and SH_TELL == 2; + assert SHAPE_ATTEMPT_LIMIT > 0 and SHAPES_PER_ROUND > 0; + assert MAX_BOUNDARY_PATHS > 0; +} diff --git a/apps/website/public/widgets/hive-shape/tool.js b/apps/website/public/widgets/hive-shape/tool.js new file mode 100644 index 0000000000..9af1958afb --- /dev/null +++ b/apps/website/public/widgets/hive-shape/tool.js @@ -0,0 +1,37 @@ +// hive-shape/tool.js -- lesson 1 of queen-and-hive: what the Queen does with an issue that is not +// ready. It decides nothing itself: the column, the row of attempts and the tiles are shape_action +// and attempt_due of gHashTag/t27 specs/queen/shaping.t27, compiled to wasm (./logic.js). Words are +// SAY_ and numbers K_ constants of specs/widgets/hive-shape.t27. +import { attemptDue, shapeAction } from './logic.js' +import { K, W, ask, board, controls, el, head, pill, root, say, source, tiles, verdict } from '../hive-kit.js' + +const TONES = [8, 0, 1] +const ctl = controls([ + { key: 'missing', label: say('SAY_MISSING'), min: 0, max: K('K_MISSING_MAX', 3), step: 1 }, + { key: 'attempts', label: say('SAY_ATTEMPTS'), min: 0, max: K('K_ATTEMPTS_MAX', 4), step: 1 }, + { key: 'minutes', label: say('SAY_MINUTES'), min: 0, max: K('K_MINUTES_MAX', 720), step: K('K_MINUTES_STEP', 30) }, + { key: 'container', label: say('SAY_CONTAINER'), kind: 'toggle' }, + { key: 'told', label: say('SAY_TOLD'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { missing: K('K_PRESET_MISSING', []), container: K('K_PRESET_CONTAINER', []), attempts: K('K_PRESET_ATTEMPTS', []), told: K('K_PRESET_TOLD', []), minutes: K('K_PRESET_MINUTES', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) + +function draw(s) { + const act = shapeAction(s.missing, !!s.container, s.attempts, !!s.told) + const row = [0, 1, 2, 3, 4].map((a) => shapeAction(s.missing, !!s.container, a, !!s.told)) + const due = attemptDue(s.attempts, s.minutes) + out.replaceChildren( + verdict(say('SAY_ACTIONS', act), TONES[act], say('SAY_ACTION_LINES', act)), + board([0, 1, 2].map((i) => ({ name: say('SAY_ACTIONS', i), tone: TONES[i] })), [{ col: act, title: say('SAY_ISSUE'), sub: say('SAY_ACTION_LINES', act), on: true }]), + head(say('SAY_ROW')), + el('p', { class: 'hk-taps' }, row.map((a, i) => pill(`${i}: ${say('SAY_ACTIONS', a)}`, TONES[a], i === s.attempts ? 'hk-on' : ''))), + tiles([ + { label: say('SAY_ACTION'), value: say('SAY_ACTIONS', act), tone: TONES[act] === 8 ? 2 : TONES[act] }, + { label: say('SAY_DUE'), value: due ? say('SAY_YES') : say('SAY_NO'), tone: due ? 0 : 6 }, + { label: say('SAY_LIMIT'), value: String(K('K_SHAPE_ATTEMPT_LIMIT', 2)), tone: 2 }, + { label: say('SAY_MINUTES'), value: String(s.minutes), tone: 5 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-ship/card.png b/apps/website/public/widgets/hive-ship/card.png new file mode 100644 index 0000000000..efd3a44965 Binary files /dev/null and b/apps/website/public/widgets/hive-ship/card.png differ diff --git a/apps/website/public/widgets/hive-ship/hive-ship.t27 b/apps/website/public/widgets/hive-ship/hive-ship.t27 new file mode 100644 index 0000000000..264087dfd5 --- /dev/null +++ b/apps/website/public/widgets/hive-ship/hive-ship.t27 @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-ship.t27 -- The game: where a bee flies and how it leaves +; Source of truth for public/widgets/hive-ship/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-ship/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/hive.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 8 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets queued, working, the cell (placed, pending, none), departing, and +; the milliseconds since it left. The ship is drawn where ship_mode says, at the opacity ship_alpha +; gives in thousandths. + +; WHAT IT DOES NOT CLAIM: it is a flat sketch of the 3D game at app.t27.ai/game, not the game. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_ship; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-ship"; +pub const TITLE : str = "The game: where a bee flies and how it leaves"; +pub const DESCRIPTION : str = "Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out."; +pub const IMAGE_ALT : str = "Ship card: a map with the hive entrance on the left, a repository portal in the middle and an issue cell on the right. The bee has left the feed 1200 ms ago: it flies HOME along the path at half opacity, 500 of 1000. Tiles read mode HOME, opacity 500, fade 2400 ms."; +pub const HOOK : str = "Before this spec a bee that left the feed vanished in the frame it left. Now it flies home and fades over 2400 ms, so a finished task has an ending on screen."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is ship_mode and ship_alpha, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 8 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MS_MAX : u16 = 3000; +pub const K_MS_STEP : u8 = 100; +pub const K_DEPART_MS : u32 = 2400; +pub const K_PRESET_QUEUED : [5]u8 = [0, 0, 0, 1, 0]; +pub const K_PRESET_WORKING : [5]u8 = [1, 1, 0, 0, 1]; +pub const K_PRESET_CELL : [5]u8 = [0, 0, 0, 0, 1]; +pub const K_PRESET_DEPARTING : [5]u8 = [1, 0, 0, 0, 0]; +pub const K_PRESET_MS : [5]u16 = [1200, 0, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Task over, flying home", "Working on its cell", "Silent too long", "Not claimed yet", "Cell not placed"]; +pub const SAY_ASK : str = "Where does a bee fly in the game, and what happens when its task ends?"; +pub const SAY_QUEUED : str = "not claimed yet"; +pub const SAY_WORKING : str = "heard from lately"; +pub const SAY_CELL : str = "its issue cell"; +pub const SAY_CELL_NAMES : [3]str = ["placed", "pending", "none"]; +pub const SAY_DEPARTING : str = "left the feed"; +pub const SAY_MS : str = "ms since it left"; +pub const SAY_MODE_SHORT : [5]str = ["PARK", "ORBIT", "HOVER", "PORTAL", "HOME"]; +pub const SAY_MODE_NAMES : [5]str = ["PARK at the entrance", "ORBIT its cell", "HOVER at the rim", "PORTAL of its repository", "HOME, fading"]; +pub const SAY_PLACES : [3]str = ["entrance", "portal", "issue cell"]; +pub const SAY_MAP : str = "the map: entrance, portal, cell"; +pub const SAY_MODE : str = "mode"; +pub const SAY_ALPHA : str = "opacity of 1000"; +pub const SAY_FADE : str = "fade, ms"; +pub const SAY_NOTE : str = "A quiet bee hovers instead of orbiting: one that has not spoken for minutes is not drawn as busy. A bee whose cell is not on the map yet waits at its repository portal, so where it is going is already true."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/hive.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_covers_the_fade { + assert K_MS_MAX > K_DEPART_MS; + assert K_DEPART_MS == 2400; +} + +test the_start_is_halfway_home { + assert K_PRESET_DEPARTING[0] == 1; + assert K_PRESET_MS[0] * 2 == K_DEPART_MS; +} diff --git a/apps/website/public/widgets/hive-ship/index.html b/apps/website/public/widgets/hive-ship/index.html new file mode 100644 index 0000000000..dfb546e0d7 --- /dev/null +++ b/apps/website/public/widgets/hive-ship/index.html @@ -0,0 +1,47 @@ + + + + + + +The game: where a bee flies and how it leaves · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

The game: where a bee flies and how it leaves

+

Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is ship_mode and ship_alpha, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 8 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-ship/logic.js b/apps/website/public/widgets/hive-ship/logic.js new file mode 100644 index 0000000000..8f8ce58bb4 --- /dev/null +++ b/apps/website/public/widgets/hive-ship/logic.js @@ -0,0 +1,44 @@ +// Generated by scripts/t27-logic.mjs from hive.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 44417077b4fe76f32540d55d0b983a15ecf6cdef514ef8859165474719a0aa71 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-ship/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABGwRgA39/fwF/YAF/AX9gAn9/AX9gBH9/f38BfwMHBgABAgIBAwQFAXABAQEFAwEAEQYJAX8BQYCAwAALB28HBm1lbW9yeQIADXQyN19jZWxsc19kdWUAAA90MjdfZmxhc2hfYWxwaGEAAQ90MjdfY2VsbF9lZmZlY3QAAg50Mjdfc2hpcF9hbHBoYQADDXQyN19zaGlwX2tlcHQABA10Mjdfc2hpcF9tb2RlAAUK6wEGGAACQCAADQBBAA8LIAFBAXMgAkGrAktyCyUBAX9BACEBAkAgAEGHDksNAEHoByAAQegHbEGIDm5rIQELIAELMQEBf0EAIQICQCAAQQlLDQAgAC0AgIDAgAAiACAAQQMgAUEBcRsgAEECRxshAgsgAgssAAJAIABBAXENAEHoBw8LAkAgAUHfEk0NAEEADwtB6AcgAUHoB2xB4BJuawsIACAAQeASSQtCAQF/QQRBACADQQFxIgQbIQMCQCAEDQAgAEEBcQ0AQQMhAwJAAkAgAg4CAQIAC0EADwtBAUECIAFBAXEbIQMLIAMLCxMBAEGAgMAACwoBAAAEAAIABQAA"; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function shipMode(queued, working, cell, departing) { + return W.t27_ship_mode(queued ? 1 : 0, working ? 1 : 0, int(cell, 0, 255, "ship_mode.cell"), departing ? 1 : 0) & 255; +} +function shipKept(msSinceLeft) { + return (W.t27_ship_kept(int(msSinceLeft, 0, 4294967295, "ship_kept.ms_since_left")) & 1) !== 0; +} +function shipAlpha(departing, msSinceLeft) { + return W.t27_ship_alpha(departing ? 1 : 0, int(msSinceLeft, 0, 4294967295, "ship_alpha.ms_since_left")) >>> 0; +} +function cellEffect(kind, accepted) { + return W.t27_cell_effect(int(kind, 0, 255, "cell_effect.kind"), accepted ? 1 : 0) & 255; +} +function flashAlpha(msSince) { + return W.t27_flash_alpha(int(msSince, 0, 4294967295, "flash_alpha.ms_since")) >>> 0; +} +function cellsDue(pending, placedBefore, secondsSinceBatch) { + return (W.t27_cells_due(int(pending, 0, 4294967295, "cells_due.pending"), placedBefore ? 1 : 0, int(secondsSinceBatch, 0, 4294967295, "cells_due.seconds_since_batch")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + cellEffect, + cellsDue, + flashAlpha, + shipAlpha, + shipKept, + shipMode +}; diff --git a/apps/website/public/widgets/hive-ship/logic.t27 b/apps/website/public/widgets/hive-ship/logic.t27 new file mode 100644 index 0000000000..ec636503bf --- /dev/null +++ b/apps/website/public/widgets/hive-ship/logic.t27 @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/hive.t27 -- how the game draws the swarm: where each ship flies, what an event does to +; a cell, how a ship leaves, and how often the map may grow +; Owner, 2026-10-08 (translated): the bees fly over the GitHub issue each is working on, in the game +; (https://app.t27.ai/game); the ships come from the same 3D kit as the comb. Epic #7718, slice 4. +; What the game did before this card (gHashTag/trinity QueenCombBabylon, measured that day): +; - a bee that left the feed vanished in the frame it left: a finished task had no ending on screen. +; - a bee on an issue with no cell yet waited at the hive's entrance, in the middle of the map, far +; from the repository it works for. +; - every newly running issue the atlas did not hold added one cell, and a new cell rebuilt the whole +; scene: one rebuild for most bees that started. +; - nothing on the map answered an event: an accepted task, a lost lease and a cancel looked the same +; as no news. +; The event kinds are control.t27's EV_* (index of a per-kind table here). Bee states arrive as two +; flags rather than tasks.t27's numbers, so no number is restated. The game runs this card as wasm. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenHive; + +pub const HIVE_KINDS : u8 = 10; + +; --- 1. Where a ship flies --------------------------------------------------------------------------- + +/// Where its issue's cell stands: on the map, waiting for the next batch (section 4), or nowhere this +/// map can show (a repository with no region, or a task that is not an issue). +pub const CELL_PLACED : u8 = 0; +pub const CELL_PENDING : u8 = 1; +pub const CELL_NONE : u8 = 2; + +/// PARK: in a row at the hive's entrance. ORBIT: circling its issue's rim. HOVER: still at the rim, +/// heard from too long ago to be drawn working. PORTAL: at its repository's portal, until its cell is +/// placed. HOME: flying back to the entrance, its task over. +pub const SM_PARK : u8 = 0; +pub const SM_ORBIT : u8 = 1; +pub const SM_HOVER : u8 = 2; +pub const SM_PORTAL : u8 = 3; +pub const SM_HOME : u8 = 4; + +/// `queued`: not claimed yet. `working`: heard from lately (tasks.t27 bee_state). `departing`: no +/// longer in the feed. +pub fn ship_mode(queued: bool, working: bool, cell: u8, departing: bool) -> u8 { + if (departing) { return SM_HOME; } + if (queued) { return SM_PARK; } + if (cell == CELL_PENDING) { return SM_PORTAL; } + if (cell != CELL_PLACED) { return SM_PARK; } + if (working) { return SM_ORBIT; } + return SM_HOVER; +} + +; --- 2. How a ship leaves ---------------------------------------------------------------------------- + +/// A ship whose bee left the feed flies home and fades out over DEPART_MS, instead of vanishing. +pub const DEPART_MS : u32 = 2400; + +pub fn ship_kept(ms_since_left: u32) -> bool { + return ms_since_left < DEPART_MS; +} + +/// Opacity in thousandths: whole while its bee is in the feed, then falling to nothing. +pub fn ship_alpha(departing: bool, ms_since_left: u32) -> u32 { + if (departing == false) { return 1000; } + if (ms_since_left >= DEPART_MS) { return 0; } + return 1000 - (ms_since_left * 1000) / DEPART_MS; +} + +; --- 3. What an event does to a cell ----------------------------------------------------------------- + +/// FX_ACCEPT gold, FX_SEND_BACK amber, FX_LOST red (the lease expired: the bee went silent and the +/// task was taken back), FX_CANCEL grey (a person stopped it), FX_START white at the cell the bee is +/// headed for. task.ended draws nothing on the cell: its ship flies home, which says it. +pub const FX_NONE : u8 = 0; +pub const FX_START : u8 = 1; +pub const FX_ACCEPT : u8 = 2; +pub const FX_SEND_BACK : u8 = 3; +pub const FX_LOST : u8 = 4; +pub const FX_CANCEL : u8 = 5; + +/// Per kind (control.t27 EV_*): created START, lease.expired LOST, reviewed ACCEPT or SEND_BACK by +/// its verdict, cancel CANCEL; everything else nothing. +pub const FX_OF_KIND : [10]u8 = [1, 0, 0, 4, 0, 2, 0, 5, 0, 0]; + +/// `accepted`: a review's verdict was an acceptance. Only a review reads it. +pub fn cell_effect(kind: u8, accepted: bool) -> u8 { + if (kind >= HIVE_KINDS) { return FX_NONE; } + let fx : u8 = FX_OF_KIND[kind]; + if (fx == FX_ACCEPT and accepted == false) { return FX_SEND_BACK; } + return fx; +} + +/// A flash rises at once and fades over FLASH_MS; an event older than that is not drawn. +pub const FLASH_MS : u32 = 1800; + +pub fn flash_alpha(ms_since: u32) -> u32 { + if (ms_since >= FLASH_MS) { return 0; } + return 1000 - (ms_since * 1000) / FLASH_MS; +} + +; --- 4. How often the map may grow ------------------------------------------------------------------- + +/// A running issue the atlas does not hold needs a cell, and a cell is part of the built scene: adding +/// one rebuilds it. So new cells are placed in batches, at most one batch every CELL_BATCH_SECONDS -- +/// the same period a board takes a whole snapshot (events.t27 SNAPSHOT_EVERY_SECONDS) -- and all of +/// them at once when the page first opens. Until its cell is placed, a bee waits at its repository's +/// portal (SM_PORTAL), so where it is going is already true. +pub const CELL_BATCH_SECONDS : u32 = 300; + +pub fn cells_due(pending: u32, placed_before: bool, seconds_since_batch: u32) -> bool { + if (pending == 0) { return false; } + if (placed_before == false) { return true; } + return seconds_since_batch >= CELL_BATCH_SECONDS; +} + +test a_ship_flies_where_its_bee_is { + // queued: at the entrance, wherever its cell stands + assert ship_mode(true, false, CELL_PLACED, false) == SM_PARK; + assert ship_mode(true, true, CELL_PENDING, false) == SM_PARK; + // claimed, its cell placed: orbit while heard, hover when quiet + assert ship_mode(false, true, CELL_PLACED, false) == SM_ORBIT; + assert ship_mode(false, false, CELL_PLACED, false) == SM_HOVER; + // its cell not placed yet: at its repository's portal, working or not + assert ship_mode(false, true, CELL_PENDING, false) == SM_PORTAL; + assert ship_mode(false, false, CELL_PENDING, false) == SM_PORTAL; + // no cell this map can show: back at the entrance + assert ship_mode(false, true, CELL_NONE, false) == SM_PARK; + // gone from the feed: home, whatever it was + assert ship_mode(false, true, CELL_PLACED, true) == SM_HOME; + assert ship_mode(true, false, CELL_NONE, true) == SM_HOME; +} + +test a_ship_leaves_by_flying_home { + assert ship_kept(0) == true; + assert ship_kept(DEPART_MS - 1) == true; + assert ship_kept(DEPART_MS) == false; + assert ship_alpha(false, 0) == 1000; + assert ship_alpha(false, 99999) == 1000; + assert ship_alpha(true, 0) == 1000; + assert ship_alpha(true, 1200) == 500; + assert ship_alpha(true, DEPART_MS - 1) == 1; + assert ship_alpha(true, DEPART_MS) == 0; + assert ship_alpha(true, 4000000000) == 0; +} + +test an_event_lights_its_cell { + assert cell_effect(0, false) == FX_START; + assert cell_effect(1, true) == FX_NONE; + assert cell_effect(2, false) == FX_NONE; + assert cell_effect(3, false) == FX_LOST; + assert cell_effect(4, true) == FX_NONE; + assert cell_effect(5, true) == FX_ACCEPT; + assert cell_effect(5, false) == FX_SEND_BACK; + assert cell_effect(6, true) == FX_NONE; + assert cell_effect(7, false) == FX_CANCEL; + assert cell_effect(8, false) == FX_NONE; + assert cell_effect(9, true) == FX_NONE; + // only a review reads the verdict + assert cell_effect(3, true) == FX_LOST; + assert cell_effect(HIVE_KINDS, true) == FX_NONE; +} + +test a_flash_fades { + assert flash_alpha(0) == 1000; + assert flash_alpha(900) == 500; + assert flash_alpha(FLASH_MS - 1) == 1; + assert flash_alpha(FLASH_MS) == 0; + assert flash_alpha(4000000000) == 0; +} + +test the_map_grows_in_batches { + assert cells_due(0, false, 0) == false; + assert cells_due(0, true, 99999) == false; + // the first answer places every cell at once + assert cells_due(3, false, 0) == true; + // after that, one batch a period + assert cells_due(1, true, CELL_BATCH_SECONDS - 1) == false; + assert cells_due(1, true, CELL_BATCH_SECONDS) == true; +} + +invariant one_row_per_kind { + assert HIVE_KINDS == 10; + assert FX_OF_KIND[5] == FX_ACCEPT; + assert FLASH_MS < DEPART_MS * 2; + assert CELL_BATCH_SECONDS == 300; +} diff --git a/apps/website/public/widgets/hive-ship/tool.js b/apps/website/public/widgets/hive-ship/tool.js new file mode 100644 index 0000000000..3c40c3546f --- /dev/null +++ b/apps/website/public/widgets/hive-ship/tool.js @@ -0,0 +1,50 @@ +// hive-ship/tool.js -- lesson 8 of queen-and-hive: where a bee flies in the game and how it leaves. +// The ship is drawn at the place ship_mode names and at the opacity ship_alpha gives, both fn bodies +// of gHashTag/t27 specs/queen/hive.t27 compiled to wasm (./logic.js); the map is a fixed layout. +// Words are SAY_ and numbers K_ constants of specs/widgets/hive-ship.t27. +import { shipAlpha, shipMode } from './logic.js' +import { COLORS, K, W, ask, canvas, controls, el, root, say, source, svgEl, tiles, verdict } from '../hive-kit.js' + +const ctl = controls([ + { key: 'cell', label: say('SAY_CELL'), kind: 'choice', options: W.SAY_CELL_NAMES }, + { key: 'ms', label: say('SAY_MS'), min: 0, max: K('K_MS_MAX', 3000), step: K('K_MS_STEP', 100) }, + { key: 'queued', label: say('SAY_QUEUED'), kind: 'toggle' }, + { key: 'working', label: say('SAY_WORKING'), kind: 'toggle' }, + { key: 'departing', label: say('SAY_DEPARTING'), kind: 'toggle' }, +], { names: W.SAY_PRESET_NAMES, values: { queued: K('K_PRESET_QUEUED', []), working: K('K_PRESET_WORKING', []), cell: K('K_PRESET_CELL', []), departing: K('K_PRESET_DEPARTING', []), ms: K('K_PRESET_MS', []) } }, draw) + +const svg = canvas('hk-map', say('SAY_MAP')) +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, svg, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +// Layout only: where each mode is drawn on the flat map, and the tone of each mode. +const ENTRANCE = [70, 120], PORTAL = [240, 120], CELL = [390, 120] +const MODE_TONES = [2, 0, 1, 5, 7] + +function draw(s) { + const mode = shipMode(!!s.queued, !!s.working, s.cell, !!s.departing) + const alpha = shipAlpha(!!s.departing, s.ms) + const fade = K('K_DEPART_MS', 2400) + svg.replaceChildren() + svg.setAttribute('viewBox', '0 0 480 220') + svg.append(svgEl('path', { d: `M${ENTRANCE[0]},${ENTRANCE[1]} L${PORTAL[0]},${PORTAL[1]} L${CELL[0]},${CELL[1]}`, stroke: 'rgba(173,201,192,0.25)', 'stroke-width': 3, 'stroke-dasharray': '6 8', fill: 'none' })) + const hex = (x, y, r) => [...Array(6)].map((_, k) => { const a = (Math.PI / 3) * k + Math.PI / 6; return `${(x + r * Math.cos(a)).toFixed(1)},${(y + r * Math.sin(a)).toFixed(1)}` }).join(' ') + svg.append(svgEl('rect', { x: ENTRANCE[0] - 50, y: ENTRANCE[1] - 40, width: 100, height: 80, rx: 18, fill: '#0b1210', stroke: COLORS[2], 'stroke-width': 3 })) + svg.append(svgEl('circle', { cx: PORTAL[0], cy: PORTAL[1], r: 38, fill: 'none', stroke: COLORS[5], 'stroke-width': 4, 'stroke-dasharray': s.cell === 1 ? '0' : '4 6' })) + svg.append(svgEl('polygon', { points: hex(CELL[0], CELL[1], 48), fill: s.cell === 0 ? '#10261c' : '#0b1210', stroke: s.cell === 0 ? COLORS[0] : 'rgba(173,201,192,0.3)', 'stroke-width': 3 })) + svg.append(svgEl('circle', { cx: CELL[0], cy: CELL[1], r: 70, fill: 'none', stroke: 'rgba(0,255,136,0.25)', 'stroke-width': 2, 'stroke-dasharray': '3 6' })) + ;[ENTRANCE, PORTAL, CELL].forEach((p, i) => svg.append(svgEl('text', { x: p[0], y: 212, 'text-anchor': 'middle', class: 'dk-txt-big' }, say('SAY_PLACES', i)))) + const t = Math.min(1, s.ms / fade) + const at = [[ENTRANCE[0], ENTRANCE[1] + 4], [CELL[0], CELL[1] - 70], [CELL[0] + 50, CELL[1] - 50], [PORTAL[0], PORTAL[1]], [CELL[0] + (ENTRANCE[0] - CELL[0]) * t, CELL[1] - 40 * Math.sin(Math.PI * t)]][mode] + svg.append(svgEl('circle', { cx: at[0], cy: at[1], r: 20, fill: COLORS[1], opacity: (alpha / 1000) * 0.35 })) + svg.append(svgEl('ellipse', { cx: at[0], cy: at[1], rx: 14, ry: 10, fill: COLORS[1], stroke: '#020403', 'stroke-width': 3, opacity: alpha / 1000 })) + out.replaceChildren( + verdict(say('SAY_MODE_NAMES', mode), MODE_TONES[mode]), + tiles([ + { label: say('SAY_MODE'), value: say('SAY_MODE_SHORT', mode), tone: MODE_TONES[mode] === 7 ? 1 : MODE_TONES[mode] }, + { label: say('SAY_ALPHA'), value: String(alpha), tone: 1 }, + { label: say('SAY_FADE'), value: String(fade), tone: 2 }, + { label: say('SAY_MS'), value: String(s.ms), tone: 5 }, + ])) +} + +ctl.start() diff --git a/apps/website/public/widgets/hive-task-filter/card.png b/apps/website/public/widgets/hive-task-filter/card.png new file mode 100644 index 0000000000..6ff465d8a3 Binary files /dev/null and b/apps/website/public/widgets/hive-task-filter/card.png differ diff --git a/apps/website/public/widgets/hive-task-filter/hive-task-filter.t27 b/apps/website/public/widgets/hive-task-filter/hive-task-filter.t27 new file mode 100644 index 0000000000..771bb5bc61 --- /dev/null +++ b/apps/website/public/widgets/hive-task-filter/hive-task-filter.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-task-filter.t27 -- One task, three kinds: the filter the board and the game share +; Source of truth for public/widgets/hive-task-filter/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-task-filter/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/tasks.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 3 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps kinds and states into two masks; every cell of the 3 x 7 grid is +; task_matches(kind mask, state mask, kind, state). The two rows below map a job state and a review +; state to a task state with job_state and review_state. + +; WHAT IT DOES NOT CLAIM: it does not read the live board: the grid is every kind against every +; state. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_task_filter; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-task-filter"; +pub const TITLE : str = "One task, three kinds: the filter the board and the game share"; +pub const DESCRIPTION : str = "An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through."; +pub const IMAGE_ALT : str = "Task filter card: kinds review and job and states running and failed are pressed. A 3 by 7 grid lights four cells, review and job in running and failed, and leaves the rest dark. Below, a job running, done, failed, cancelled reads running, done, failed, dropped."; +pub const HOOK : str = "An issue, a job and a review each kept their own codes. Now one filter, a bit per kind and a bit per state, serves the board, the game and the route."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/tasks.t27 (t27 master c42f8fb40): every value on the page is task_matches, job_state and review_state, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 3 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KINDS : u8 = 3; +pub const K_STATES : u8 = 7; +pub const K_TASK_KINDS : u8 = 3; +pub const K_TASK_STATES : u8 = 7; +pub const K_PRESET_KINDS : [4]u8 = [6, 0, 1, 0]; +pub const K_PRESET_STATES : [4]u8 = [68, 0, 4, 8]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Reviews and jobs, running or failed", "Everything (0 means any)", "Issues running", "Anything in review"]; +pub const SAY_ASK : str = "How can one board show issues, reviews and jobs at once?"; +pub const SAY_KIND_NAMES : [3]str = ["issue", "review", "job"]; +pub const SAY_STATE_NAMES : [7]str = ["backlog", "blocked", "running", "review", "done", "dropped", "failed"]; +pub const SAY_KINDS : str = "kinds"; +pub const SAY_STATES : str = "states"; +pub const SAY_GRID : str = "kind by state: lit means the filter lets it through"; +pub const SAY_JOB : str = "a job that is"; +pub const SAY_JOB_STATES : [4]str = ["running", "done", "failed", "cancelled"]; +pub const SAY_REVIEW : str = "a review row that is"; +pub const SAY_REVIEW_STATES : [4]str = ["queued", "posted", "forgotten", "failed"]; +pub const SAY_SHOWS : str = "shows as"; +pub const SAY_PASS : str = "in"; +pub const SAY_NOTE : str = "A mask of 0 means any. A job and a review keep their own machines; this spec only says which of the seven states each one shows as, so the board, the game and the route filter alike."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/tasks.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_grid_is_every_kind_by_every_state { + assert K_KINDS == K_TASK_KINDS; + assert K_STATES == K_TASK_STATES; + assert K_KINDS * K_STATES == 21; +} + +test the_masks_fit_their_bits { + assert K_PRESET_KINDS[0] < 8; + assert K_PRESET_STATES[0] < 128; + assert K_PRESET_KINDS[1] == 0; +} diff --git a/apps/website/public/widgets/hive-task-filter/index.html b/apps/website/public/widgets/hive-task-filter/index.html new file mode 100644 index 0000000000..25cbc33fd7 --- /dev/null +++ b/apps/website/public/widgets/hive-task-filter/index.html @@ -0,0 +1,47 @@ + + + + + + +One task, three kinds: the filter the board and the game share · Trinity S³AI + + + + + + + + + + + + + + + + + + + + + + + + +
+Trinity S³AI +

One task, three kinds: the filter the board and the game share

+

An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through.

+
+
+ +
+

This page sets no cookie and sends nothing anywhere.

+
Where the numbers come from
  • gHashTag/t27 specs/queen/tasks.t27 (t27 master c42f8fb40): every value on the page is task_matches, job_state and review_state, its fn bodies compiled to wasm (logic.js)
  • Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 3 opens this widget
+

This widget's .t27 source · All widgets

+
+ + + + diff --git a/apps/website/public/widgets/hive-task-filter/logic.js b/apps/website/public/widgets/hive-task-filter/logic.js new file mode 100644 index 0000000000..6505591d14 --- /dev/null +++ b/apps/website/public/widgets/hive-task-filter/logic.js @@ -0,0 +1,40 @@ +// Generated by scripts/t27-logic.mjs from tasks.t27. Do not edit. +// +// The spec's own fn bodies, compiled: t27c gen (Zig) -> zig 0.16.0 (wasm32). +// Change the spec in gHashTag/t27, vendor it, regenerate. A rule written +// by hand beside this file is the thing this file exists to end. +// spec-sha256: 8a0e16fb90725445cca03d5b87b2dacf4f2b9c33d27273e60f1c647c07ee7058 +// Imported by scripts/widget-logic.mjs for public/widgets/hive-task-filter/: types stripped, nothing else changed. +const WASM = "AGFzbQEAAAABFANgBH9/f38Bf2ACf38Bf2ABfwF/AwYFAAECAgIEBQFwAQEBBQMBABAGCQF/AUGAgMAACwdgBgZtZW1vcnkCABB0MjdfdGFza19tYXRjaGVzAAANdDI3X2JlZV9zdGF0ZQABDXQyN19hMmFfc3RhdGUAAhB0MjdfcmV2aWV3X3N0YXRlAAMNdDI3X2pvYl9zdGF0ZQAECqQBBT8BAX9BACEEAkAgAkECSw0AIANBBksNAAJAIABFDQAgACACdkEBcUUNAQsgAUVBASADdCABcUEAR3IhBAsgBAsVAEECQQEgAUG0AUsbQQAgAEEBcRsLHQBCgYaImMCgASAArUIDhoinQQYgAEEGSRtBB3ELFwBBgIgUIABBA3R2QQYgAEEDSRtBB3ELFgAgAEEBdEECakEFIABBA0kbQf8BcQs="; +const W = new WebAssembly.Instance( + new WebAssembly.Module(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))) +).exports; +function int(v, lo, hi, at) { + if (!Number.isInteger(v) || v < lo || v > hi) + throw new RangeError(`${at}: ${v} is outside ${lo}..${hi}`); + return v; +} +function jobState(j) { + return W.t27_job_state(int(j, 0, 255, "job_state.j")) & 255; +} +function reviewState(r) { + return W.t27_review_state(int(r, 0, 255, "review_state.r")) & 255; +} +function a2aState(s) { + return W.t27_a2a_state(int(s, 0, 255, "a2a_state.s")) & 255; +} +function beeState(claimed, secondsSince) { + return W.t27_bee_state(claimed ? 1 : 0, int(secondsSince, 0, 4294967295, "bee_state.seconds_since")) & 255; +} +function taskMatches(kindMask, stateMask, kind, state) { + return (W.t27_task_matches(int(kindMask, 0, 4294967295, "task_matches.kind_mask"), int(stateMask, 0, 4294967295, "task_matches.state_mask"), int(kind, 0, 255, "task_matches.kind"), int(state, 0, 255, "task_matches.state")) & 1) !== 0; +} +const __NOT_LOWERED__ = Object.freeze([]); +export { + __NOT_LOWERED__, + a2aState, + beeState, + jobState, + reviewState, + taskMatches +}; diff --git a/apps/website/public/widgets/hive-task-filter/logic.t27 b/apps/website/public/widgets/hive-task-filter/logic.t27 new file mode 100644 index 0000000000..7443ffb54a --- /dev/null +++ b/apps/website/public/widgets/hive-task-filter/logic.t27 @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/queen/tasks.t27 -- one task, many kinds, and the bees working on them, as everyone reads them +; Owner, 2026-10-08 (translated): lower every task of the project to the scheduler's native level; a +; dashboard shows all of it with any filter (https://app.t27.ai/game/kanban); the bees fly over the +; GitHub issue each is working on in the game (https://app.t27.ai/game). Epic #7718, slice 1. +; Measured that morning, four surveys: +; - there is no single task. An issue dispatch (dispatch.t27 COL_*), a job (jobs.t27 J_*) and an app +; review (app.t27, per head) each kept their own codes. +; - no public field said which bee is on which issue. The game drew a public snapshot with no bees, and +; its older comb paired the k-th busy slot with the k-th running card, which is a rank, not a link. +; This card is the view everyone reads: the supervisor builds GET /queen/public-tasks with it, and the +; board and the game filter and draw with it. The kinds keep their own machines; this only says how +; each one shows. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module QueenTasks; + +/// Kinds of task. Index into KIND_NAMES. +pub const TK_ISSUE : u8 = 0; +pub const TK_REVIEW : u8 = 1; +pub const TK_JOB : u8 = 2; +pub const TASK_KINDS : u8 = 3; +pub const KIND_NAMES : [3]str = ["issue", "review", "job"]; + +/// One state for every kind, numbered as dispatch.t27's board columns (COL_BACKLOG .. COL_DROPPED) so +/// an issue's column IS its state, plus FAILED, which a column never was. Index into STATE_NAMES. +pub const TS_BACKLOG : u8 = 0; +pub const TS_BLOCKED : u8 = 1; +pub const TS_RUNNING : u8 = 2; +pub const TS_REVIEW : u8 = 3; +pub const TS_DONE : u8 = 4; +pub const TS_DROPPED : u8 = 5; +pub const TS_FAILED : u8 = 6; +pub const TASK_STATES : u8 = 7; +pub const STATE_NAMES : [7]str = ["backlog", "blocked", "running", "review", "done", "dropped", "failed"]; + +/// A job's state (jobs.t27: 0 running, 1 done, 2 failed, 3 cancelled) as a task state. +pub fn job_state(j: u8) -> u8 { + if (j == 0) { return TS_RUNNING; } + if (j == 1) { return TS_DONE; } + if (j == 2) { return TS_FAILED; } + return TS_DROPPED; +} + +/// An app review's row state (queued 0, posted 1, forgotten 2, failed 3) as a task state. +pub fn review_state(r: u8) -> u8 { + if (r == 0) { return TS_BACKLOG; } + if (r == 1) { return TS_DONE; } + if (r == 2) { return TS_DROPPED; } + return TS_FAILED; +} + +/// The A2A v1.0 task state each state is, for the day the Queen answers A2A at her edge (epic #7718 +/// slice 7). A task waiting for a judge or a person is input-required; dropped is canceled. +pub const A2A_SUBMITTED : u8 = 1; +pub const A2A_WORKING : u8 = 2; +pub const A2A_INPUT_REQUIRED : u8 = 3; +pub const A2A_COMPLETED : u8 = 4; +pub const A2A_CANCELED : u8 = 5; +pub const A2A_FAILED : u8 = 6; + +pub fn a2a_state(s: u8) -> u8 { + if (s == TS_BACKLOG) { return A2A_SUBMITTED; } + if (s == TS_RUNNING) { return A2A_WORKING; } + if (s == TS_BLOCKED or s == TS_REVIEW) { return A2A_INPUT_REQUIRED; } + if (s == TS_DONE) { return A2A_COMPLETED; } + if (s == TS_DROPPED) { return A2A_CANCELED; } + return A2A_FAILED; +} + +/// A bee is one dispatch still in flight: offered and not claimed yet (QUEUED), heard from within +/// BEE_QUIET_SECONDS (WORKING), or silent longer (QUIET). The game draws a quiet bee hovering, not +/// working: a bee that has not spoken for minutes is not shown as busy. +pub const BS_QUEUED : u8 = 0; +pub const BS_WORKING : u8 = 1; +pub const BS_QUIET : u8 = 2; +pub const BEE_QUIET_SECONDS : u32 = 180; +pub const BEE_STATE_NAMES : [3]str = ["queued", "working", "quiet"]; + +/// `claimed`: a runner took the order (or the container started it); `heard`: an event was recorded +/// for its conversation; `seconds_since`: since that event, or since it started when none was heard. +pub fn bee_state(claimed: bool, seconds_since: u32) -> u8 { + if (claimed == false) { return BS_QUEUED; } + if (seconds_since > BEE_QUIET_SECONDS) { return BS_QUIET; } + return BS_WORKING; +} + +/// The filter the board, the game and the route all apply: a bit per kind and a bit per state, and 0 +/// meaning "any". Repository and text are matched beside it; they are strings, and equality on +/// strings is the caller's. +pub fn task_matches(kind_mask: u32, state_mask: u32, kind: u8, state: u8) -> bool { + if (kind >= TASK_KINDS or state >= TASK_STATES) { return false; } + if (kind_mask != 0 and (kind_mask & (1 << kind)) == 0) { return false; } + if (state_mask != 0 and (state_mask & (1 << state)) == 0) { return false; } + return true; +} + +/// Tasks one answer carries at most; the route pages past it. +pub const TASK_PAGE_MAX : u32 = 2000; +pub const TASK_PAGE_DEFAULT : u32 = 500; + +test a_job_and_a_review_show_as_one_lifecycle { + assert job_state(0) == TS_RUNNING; + assert job_state(1) == TS_DONE; + assert job_state(2) == TS_FAILED; + assert job_state(3) == TS_DROPPED; + assert review_state(0) == TS_BACKLOG; + assert review_state(1) == TS_DONE; + assert review_state(2) == TS_DROPPED; + assert review_state(3) == TS_FAILED; +} + +test every_state_has_an_a2a_state { + assert a2a_state(TS_BACKLOG) == A2A_SUBMITTED; + assert a2a_state(TS_RUNNING) == A2A_WORKING; + assert a2a_state(TS_BLOCKED) == A2A_INPUT_REQUIRED; + assert a2a_state(TS_REVIEW) == A2A_INPUT_REQUIRED; + assert a2a_state(TS_DONE) == A2A_COMPLETED; + assert a2a_state(TS_DROPPED) == A2A_CANCELED; + assert a2a_state(TS_FAILED) == A2A_FAILED; +} + +test a_bee_is_queued_working_or_quiet { + assert bee_state(false, 0) == BS_QUEUED; + assert bee_state(false, 9999) == BS_QUEUED; + assert bee_state(true, 0) == BS_WORKING; + assert bee_state(true, BEE_QUIET_SECONDS) == BS_WORKING; + assert bee_state(true, BEE_QUIET_SECONDS + 1) == BS_QUIET; +} + +test a_filter_narrows_and_zero_means_any { + assert task_matches(0, 0, TK_ISSUE, TS_RUNNING) == true; + assert task_matches(1 << TK_ISSUE, 0, TK_ISSUE, TS_DONE) == true; + assert task_matches(1 << TK_JOB, 0, TK_ISSUE, TS_DONE) == false; + assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_RUNNING) == true; + assert task_matches(0, 1 << TS_RUNNING, TK_REVIEW, TS_BACKLOG) == false; + assert task_matches((1 << TK_ISSUE) | (1 << TK_JOB), (1 << TS_RUNNING) | (1 << TS_REVIEW), TK_JOB, TS_REVIEW) == true; + // an unknown kind or state matches nothing, even with no filter + assert task_matches(0, 0, TASK_KINDS, TS_DONE) == false; + assert task_matches(0, 0, TK_ISSUE, TASK_STATES) == false; +} + +invariant the_states_are_the_board_columns { + assert TS_BACKLOG == 0 and TS_BLOCKED == 1 and TS_RUNNING == 2; + assert TS_REVIEW == 3 and TS_DONE == 4 and TS_DROPPED == 5 and TS_FAILED == 6; + assert TK_ISSUE == 0 and TK_REVIEW == 1 and TK_JOB == 2; + assert TASK_PAGE_DEFAULT <= TASK_PAGE_MAX; +} diff --git a/apps/website/public/widgets/hive-task-filter/tool.js b/apps/website/public/widgets/hive-task-filter/tool.js new file mode 100644 index 0000000000..666950cd6e --- /dev/null +++ b/apps/website/public/widgets/hive-task-filter/tool.js @@ -0,0 +1,31 @@ +// hive-task-filter/tool.js -- lesson 3 of queen-and-hive: one task, three kinds, seven states. Every +// grid cell is task_matches(kind mask, state mask, kind, state) and the rows below are job_state and +// review_state, all of gHashTag/t27 specs/queen/tasks.t27 compiled to wasm (./logic.js). Words are +// SAY_ and numbers K_ constants of specs/widgets/hive-task-filter.t27. +import { jobState, reviewState, taskMatches } from './logic.js' +import { K, W, ask, controls, el, grid, head, pill, root, say, source, taps } from '../hive-kit.js' + +const NK = K('K_KINDS', 3), NS = K('K_STATES', 7) +const ctl = controls([ + { key: 'km', kind: 'hidden', min: 0, max: 7 }, + { key: 'sm', kind: 'hidden', min: 0, max: 127 }, +], { names: W.SAY_PRESET_NAMES, values: { km: K('K_PRESET_KINDS', []), sm: K('K_PRESET_STATES', []) } }, draw) + +const out = el('div') +root.replaceChildren(ask(say('SAY_ASK')), ctl.node, out, el('p', { class: 'dk-note' }, say('SAY_NOTE')), source()) +const STATE_TONES = [2, 6, 0, 1, 0, 8, 6] + +function draw(s) { + out.replaceChildren( + head(say('SAY_KINDS')), + taps([...Array(NK)].map((_, i) => ({ label: say('SAY_KIND_NAMES', i), pressed: !!(s.km & (1 << i)), tone: 5 })), (i) => ctl.set('km', s.km ^ (1 << i)), say('SAY_KINDS')), + head(say('SAY_STATES')), + taps([...Array(NS)].map((_, i) => ({ label: say('SAY_STATE_NAMES', i), pressed: !!(s.sm & (1 << i)), tone: STATE_TONES[i] })), (i) => ctl.set('sm', s.sm ^ (1 << i)), say('SAY_STATES')), + head(say('SAY_GRID')), + grid({ rows: NK, cols: NS, rowLabels: W.SAY_KIND_NAMES, colLabels: W.SAY_STATE_NAMES, cell: (r, c) => { const on = taskMatches(s.km, s.sm, r, c); return { text: on ? say('SAY_PASS') : '', on, tone: STATE_TONES[c] } } }), + el('div', { class: 'hk-two' }, + el('div', {}, head(say('SAY_JOB')), el('p', { class: 'hk-taps' }, [0, 1, 2, 3].map((j) => { const t = jobState(j); return pill(`${say('SAY_JOB_STATES', j)}: ${say('SAY_STATE_NAMES', t)}`, STATE_TONES[t]) }))), + el('div', {}, head(say('SAY_REVIEW')), el('p', { class: 'hk-taps' }, [0, 1, 2, 3].map((r) => { const t = reviewState(r); return pill(`${say('SAY_REVIEW_STATES', r)}: ${say('SAY_STATE_NAMES', t)}`, STATE_TONES[t]) }))))) +} + +ctl.start() diff --git a/apps/website/scripts/course-from-spec.test.mjs b/apps/website/scripts/course-from-spec.test.mjs index 0f14b9628e..5bfc287f42 100644 --- a/apps/website/scripts/course-from-spec.test.mjs +++ b/apps/website/scripts/course-from-spec.test.mjs @@ -119,7 +119,7 @@ test('a lesson in two courses fails the build: its page can show one', async () }) test('a course left out of the catalog fails the build', async () => { - await fails({ catalogText: replaced(inputs.catalogText, 'pub const COURSE_COUNT : u8 = 10;', 'pub const COURSE_COUNT : u8 = 11;') }, 'the_basics_come_first') + await fails({ catalogText: replaced(inputs.catalogText, 'pub const COURSE_COUNT : u8 = 11;', 'pub const COURSE_COUNT : u8 = 12;') }, 'the_basics_come_first') }) test('a broken claim in a spec fails its own test block', async () => { diff --git a/apps/website/specs/course/courses.t27 b/apps/website/specs/course/courses.t27 index 5bcd8cc900..02ea5caa33 100644 --- a/apps/website/specs/course/courses.t27 +++ b/apps/website/specs/course/courses.t27 @@ -28,10 +28,10 @@ pub const SCHEMA_VERSION : u8 = 1; ; nothing (#/verify-hardware had no route). pub const GENERATED : [2]str = ["src/lib/course.generated.ts", "src/lib/courseRoutes.generated.ts"]; -pub const COURSE_COUNT : u8 = 10; +pub const COURSE_COUNT : u8 = 11; -pub const COURSE_IDS : [10]str = ["t27-basics", "course", "ai-numbers", "goldenfloat", "verify-hardware", "clocks-and-cdc", "buses-and-peripherals", "ternary-computing", "arithmetic-and-dsp", "neural-networks"]; -pub const COURSE_SPECS : [10]str = ["specs/course/t27-basics.t27", "specs/course/course.t27", "specs/course/ai-numbers.t27", "specs/course/goldenfloat.t27", "specs/course/verify-hardware.t27", "specs/course/clocks-and-cdc.t27", "specs/course/buses-and-peripherals.t27", "specs/course/ternary-computing.t27", "specs/course/arithmetic-and-dsp.t27", "specs/course/neural-networks.t27"]; +pub const COURSE_IDS : [11]str = ["t27-basics", "course", "ai-numbers", "goldenfloat", "verify-hardware", "clocks-and-cdc", "buses-and-peripherals", "ternary-computing", "arithmetic-and-dsp", "neural-networks", "queen-and-hive"]; +pub const COURSE_SPECS : [11]str = ["specs/course/t27-basics.t27", "specs/course/course.t27", "specs/course/ai-numbers.t27", "specs/course/goldenfloat.t27", "specs/course/verify-hardware.t27", "specs/course/clocks-and-cdc.t27", "specs/course/buses-and-peripherals.t27", "specs/course/ternary-computing.t27", "specs/course/arithmetic-and-dsp.t27", "specs/course/neural-networks.t27", "specs/course/queen-and-hive.t27"]; pub const LESSONS_PER_COURSE : u8 = 27; pub const MODULES_PER_COURSE : u8 = 9; @@ -50,7 +50,7 @@ test every_course_is_three_cubed { } test the_basics_come_first { - assert COURSE_COUNT == 10; + assert COURSE_COUNT == 11; assert COURSE_IDS[0] == "t27-basics"; assert COURSE_SPECS[0] == "specs/course/t27-basics.t27"; assert COURSE_IDS[1] == "course"; @@ -70,7 +70,12 @@ test arithmetic_and_dsp_follows_the_ternary_machine { assert COURSE_SPECS[8] == "specs/course/arithmetic-and-dsp.t27"; } -test neural_networks_close_the_catalog { +test neural_networks_follow_the_dsp_course { assert COURSE_IDS[9] == "neural-networks"; assert COURSE_SPECS[9] == "specs/course/neural-networks.t27"; } + +test the_queen_and_the_hive_close_the_catalog { + assert COURSE_IDS[10] == "queen-and-hive"; + assert COURSE_SPECS[10] == "specs/course/queen-and-hive.t27"; +} diff --git a/apps/website/specs/course/queen-and-hive-ru.t27 b/apps/website/specs/course/queen-and-hive-ru.t27 new file mode 100644 index 0000000000..4439f7ccce --- /dev/null +++ b/apps/website/specs/course/queen-and-hive-ru.t27 @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/course/queen-and-hive-ru.t27 -- translation contract, Russian, for the course on the Queen and the hive +; Specs are English-only (LANG-EN, L3). This module carries no translated word: it +; says WHICH fields of specs/course/queen-and-hive.t27 the Russian bundle translates, WHERE +; the bundle lives, HOW it is keyed and what the page does when a field is missing. +; The Cyrillic text is in the bundle named by BUNDLE_PATH, never in a .t27 file. +; apps/website/scripts/course-from-spec.mjs refuses the build when the bundle misses +; a field (COVERAGE_REQUIRED), carries a key the course does not have +; (ORPHANS_ALLOWED), changes a number, or drops a {placeholder}. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module queen_and_hive_ru; + +pub const KIND : str = "i18n"; +pub const LOCALE : str = "ru"; +pub const SOURCE_LOCALE : str = "en"; +pub const SOURCE_SPEC : str = "specs/course/queen-and-hive.t27"; +pub const BUNDLE_REPO : str = "trinity"; +pub const BUNDLE_PATH : str = "apps/website/i18n/queen-and-hive.ru.json"; +pub const BUNDLE_FORMAT : str = "json"; +; A module entry is keyed by its MODULE_IDS value, a lesson entry by its LESSON_IDS +; value; the page strings sit under the key UI_KEY, keyed by the SAY_ name. +pub const MODULE_FIELDS : [2]str = ["TITLE", "LINE"]; +pub const LESSON_FIELDS : [4]str = ["TITLE", "GOAL", "TEXT", "TASK"]; +pub const UI_KEY : str = "ui"; +pub const FALLBACK : str = "en"; +; true: a module, lesson or page string with no Russian fails the build. +pub const COVERAGE_REQUIRED : bool = true; +; false: a bundle key the course does not have fails the build. +pub const ORPHANS_ALLOWED : bool = false; +pub const ENABLED : bool = true; + +test the_course_is_translated_whole { + assert COVERAGE_REQUIRED == true; + assert ORPHANS_ALLOWED == false; + assert LOCALE == "ru"; + assert FALLBACK == "en"; +} diff --git a/apps/website/specs/course/queen-and-hive.t27 b/apps/website/specs/course/queen-and-hive.t27 new file mode 100644 index 0000000000..5d7850736c --- /dev/null +++ b/apps/website/specs/course/queen-and-hive.t27 @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/course/queen-and-hive.t27 -- the t27 course on the Queen and the hive, how the project builds itself +; Source of truth for apps/website/scripts/course-from-spec.mjs (gHashTag/trinity), which reads +; it the way it reads specs/course/course.t27: the constant schema, every test block below, every +; widget in specs/widgets/gallery.t27, every lesson spec compiled clean, the Russian bundle named +; by specs/course/queen-and-hive-ru.t27. It writes this course into src/lib/course.generated.ts +; (the page at #/queen-and-hive) and a byte-identical copy of this file at +; public/learn/queen-and-hive.t27. ASCII only (L3), English only (LANG-EN). + +; WHAT THE COURSE IS: course 11 of specs/course/courses.t27, 27 lessons in 9 modules of 3: the +; task, who goes next, a bee's life, the reviewer bee, the review jam, keys, credit and money, +; agents among themselves, and night and the board. The project builds itself: an issue becomes +; a task for a bee, the bee writes a .t27 spec, a reviewer bee checks it, it merges and the site +; regenerates. The Queen that runs this loop is written in gHashTag/t27 specs/queen and +; specs/automation, each spec with numeric tests. Every widget runs the fn bodies of one of 15 of +; those specs compiled to WebAssembly (public/widgets//logic.js, held to its spec by +; scripts/widget-logic.mjs), vendored byte for byte from t27 master c42f8fb40. The door code +; around review-jam's fns comes from gHashTag/999-multibots-telegraf#4020. + +; WHAT THE COURSE DOES NOT DO: it reads no live Queen. Every number in a lesson is a constant or +; a recorded measurement of the lesson spec, or an answer of its compiled fns to inputs the +; reader sets. Lesson 26 shows a finding of dispatch.t27 as the board draws it today: a closed +; issue's finished claims all land in done. + +; WHY 27: 3 cubed lessons, the size of a TRI-27 word. Changing the shape changes the tests. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module queen_and_hive; + +pub const KIND : str = "course"; +pub const ID : str = "queen-and-hive"; +pub const SCHEMA_VERSION : u8 = 1; +; The derived files the generator rewrites (relative to apps/website). +pub const GENERATED : [2]str = ["src/lib/course.generated.ts", "public/learn/queen-and-hive.t27"]; +pub const ROUTE : str = "queen-and-hive"; +; Share pages: learn/ for course 1, learn// for the next (specs/course/courses.t27). +pub const SHARE_PATH : str = "learn/queen-and-hive/"; +; The paid training every course page links to (specs/course/arithmetic-and-dsp.t27). +pub const COHORT_ROUTE : str = "fpga-training"; +pub const GALLERY : str = "specs/widgets/gallery.t27"; +pub const LOCALES : [2]str = ["en", "ru"]; +pub const RU_CONTRACT : str = "specs/course/queen-and-hive-ru.t27"; +; Lesson marks the reader sets are kept in this browser only, under this key, one key for +; every course. +pub const PROGRESS_KEY : str = "t27-course-done"; +pub const SENDS_NOTHING : bool = true; +; A lesson's widget is the page's main stage, so it is drawn taller than a +; gallery card (specs/widgets/gallery.t27 EMBED_* heights are for embeds in posts). +pub const TOOL_FRAME_HEIGHT : u16 = 760; +pub const PLAYER_FRAME_HEIGHT : u16 = 480; + +; --- Words on the page --------------------------------------------------------------------- +pub const TITLE : str = "The Queen and the hive: how the t27 project builds itself"; +pub const DESCRIPTION : str = "Course 11, 27 lessons, a live widget and a t27 spec each: shaping an issue, priority and review first, a task's life, the reviewer and the merge gate, the review jam, keys, credit and tokens, agents in one group, the night watch and the board. Every answer comes from a compiled spec."; +pub const SAY_KICKER : str = "Course"; +pub const SAY_LEAD : str = "This project builds itself: an issue becomes a task for a bee, the bee writes a t27 spec, a reviewer bee checks it, it merges, and the site regenerates. The Queen who runs that loop is written as t27 specs with tests, and every widget here runs the fn bodies of one of them compiled to WebAssembly."; +pub const SAY_SHAPE : str = "9 modules of 3 lessons, 27 cells. A filled cell is a lesson you marked done."; +pub const SAY_START : str = "Start lesson 1"; +pub const SAY_CONTINUE : str = "Continue"; +pub const SAY_MODULE : str = "Module {0}"; +pub const SAY_LESSON : str = "Lesson {0} of {1}"; +pub const SAY_GOAL : str = "You will learn"; +pub const SAY_TRY : str = "Try it"; +pub const SAY_ALSO : str = "Also try"; +pub const SAY_ALSO_HINT : str = "Each one swaps the widget above."; +pub const SAY_BACK_TO_MAIN : str = "Back to this lesson's widget"; +pub const SAY_SPEC : str = "Open the lesson's spec in the player"; +pub const SAY_OPEN_PAGE : str = "Open the widget on its own page"; +pub const SAY_WIDGET_LANG : str = "Widgets keep their own English words: each one lives in its own t27 spec."; +pub const SAY_PREV : str = "Previous"; +pub const SAY_NEXT : str = "Next"; +pub const SAY_ALL : str = "All lessons"; +pub const SAY_MARK : str = "Mark as done"; +pub const SAY_MARKED : str = "Done"; +pub const SAY_PROGRESS : str = "{0} of {1} done"; +pub const SAY_PRIVATE : str = "Progress stays in this browser; nothing is sent."; +pub const SAY_SOURCE : str = "This course is itself a t27 spec: read it"; +pub const SAY_COHORT : str = "Looking for the paid FPGA training? It moved to its own page."; +pub const SAY_NOT_FOUND : str = "No lesson has this address."; +pub const SAY_OPEN_LESSON : str = "Open the interactive lesson"; +pub const SAY_OPEN_COURSE : str = "Open the interactive course"; +pub const SAY_SEO_TITLE : str = "{0}: the Queen and the hive course, lesson {1} of {2}"; +pub const SAY_SHARE : str = "Link to share, with a preview card"; +pub const SAY_NEXT_COURSE : str = "Next course"; +pub const SAY_PREV_COURSE : str = "Previous course"; + +; --- Modules ------------------------------------------------------------------------------- +pub const LESSONS_PER_MODULE : u8 = 3; +pub const MODULE_COUNT : u8 = 9; +pub const MODULE_IDS : [9]str = ["the-task", "who-goes-next", "a-bees-life", "the-reviewer", "the-jam", "keys", "credit-and-money", "agents-together", "night-and-the-board"]; +pub const MODULE_TITLES : [9]str = [ + "The task", + "Who goes next", + "A bee's life", + "The reviewer bee", + "The review jam", + "Keys", + "Credit and money", + "Agents among themselves", + "Night and the board" +]; +pub const MODULE_LINES : [9]str = [ + "An issue becomes work only once it is shaped: the sections it lacks are written or asked for, its boundary names real files, and one filter reads every kind of task.", + "The order the Queen takes issues in: a label ages one step, at most 3 criticals run as critical, and nothing new starts while finished work waits for review.", + "From candidate to done: twelve states and sixteen events, a ship in the game that flies home, and a deploy that does not cut a bee short.", + "Finished work is judged on its own clock: enough reviews at once to keep up, a pause that doubles for a row that waits, and a merge gate that zero checks cannot open.", + "A pull request that will not move is in exactly one of ten cases, each with the Queen's move, and a red check is inherited only when master says so.", + "A bee's model key can be refused: the code in the body sorts the refusal, a transient one rests the key for seconds, and the bee moves on to the next free key.", + "Lending a key earns no credit, an operation is sold at twice its cost in half-cent tokens, and tokens burn 30 days after they land.", + "One bot per agent in one group: who is called, a chain three bot messages deep, and the clocks that keep agents from flooding a chat.", + "What the hive shows: a night watch that opens, comments on and closes issues, the columns of the public board, and the comb that flashes for each event." +]; + +; --- Lessons ------------------------------------------------------------------------------- +; LESSON_WIDGETS names the widget the lesson opens; every one is a gallery id, and no two +; lessons of any course open the same one. LESSON_ALSO offers module siblings and related hive +; widgets. LESSON_SPECS is the spec under public/t27/files/ the lesson opens in the player, and +; the spec whose compiled fn bodies the widget runs. +pub const LESSON_COUNT : u8 = 27; +pub const LESSON_IDS : [27]str = [ + "shaping-an-issue", + "a-boundary-of-real-files", + "one-task-three-kinds", + "aging-a-label", + "the-queue", + "review-first", + "a-tasks-life", + "the-ship-flies-home", + "a-deploy-mid-flight", + "enough-reviewers", + "back-off", + "the-merge-gate", + "the-review-jam", + "the-queens-move", + "inherited-red", + "a-refused-key", + "a-key-rests", + "the-next-key", + "no-credit-for-keys", + "twice-the-cost", + "tokens-that-burn", + "who-is-called", + "three-bots-deep", + "stop-and-mute", + "the-night-watch", + "the-public-board", + "the-comb-answers" +]; +pub const LESSON_MODULES : [27]str = [ + "the-task", + "the-task", + "the-task", + "who-goes-next", + "who-goes-next", + "who-goes-next", + "a-bees-life", + "a-bees-life", + "a-bees-life", + "the-reviewer", + "the-reviewer", + "the-reviewer", + "the-jam", + "the-jam", + "the-jam", + "keys", + "keys", + "keys", + "credit-and-money", + "credit-and-money", + "credit-and-money", + "agents-together", + "agents-together", + "agents-together", + "night-and-the-board", + "night-and-the-board", + "night-and-the-board" +]; +pub const LESSON_WIDGETS : [27]str = [ + "hive-shape", + "hive-boundary", + "hive-task-filter", + "hive-aging", + "hive-rank", + "hive-review-first", + "hive-lifecycle", + "hive-ship", + "hive-drain", + "hive-reviewers", + "hive-revisit", + "hive-gate", + "hive-jam", + "hive-jam-action", + "hive-master-red", + "hive-key-error", + "hive-key-rest", + "hive-next-key", + "hive-credit", + "hive-price", + "hive-burn", + "hive-called", + "hive-chain", + "hive-cooldown", + "hive-night", + "hive-column", + "hive-cell" +]; +pub const LESSON_ALSO : [27]str = [ + "hive-boundary,hive-task-filter", + "hive-shape,hive-task-filter", + "hive-shape,hive-column,hive-lifecycle", + "hive-rank,hive-review-first", + "hive-aging,hive-review-first", + "hive-rank,hive-reviewers", + "hive-ship,hive-drain,hive-column", + "hive-lifecycle,hive-cell", + "hive-lifecycle,hive-ship", + "hive-revisit,hive-gate,hive-review-first", + "hive-reviewers,hive-key-rest", + "hive-master-red,hive-reviewers", + "hive-jam-action,hive-master-red", + "hive-jam,hive-gate", + "hive-gate,hive-jam", + "hive-key-rest,hive-next-key", + "hive-key-error,hive-next-key,hive-revisit", + "hive-key-error,hive-key-rest,hive-credit", + "hive-price,hive-next-key", + "hive-burn,hive-credit", + "hive-price,hive-night", + "hive-chain,hive-cooldown", + "hive-called,hive-cooldown", + "hive-chain,hive-called", + "hive-column,hive-cell", + "hive-task-filter,hive-lifecycle,hive-night", + "hive-ship,hive-night" +]; +pub const LESSON_SPECS : [27]str = [ + "specs/queen/shaping.t27", + "specs/queen/shaping.t27", + "specs/queen/tasks.t27", + "specs/queen/priority.t27", + "specs/queen/priority.t27", + "specs/queen/review_first.t27", + "specs/queen/dispatch.t27", + "specs/queen/hive.t27", + "specs/queen/drain.t27", + "specs/queen/reviewer.t27", + "specs/queen/reviewer.t27", + "specs/queen/merger_gate.t27", + "specs/automation/review-jam.t27", + "specs/automation/review-jam.t27", + "specs/queen/merger_gate.t27", + "specs/queen/key_failover.t27", + "specs/queen/key_failover.t27", + "specs/queen/key_failover.t27", + "specs/automation/queen-contributor-keys.t27", + "specs/automation/token-wallet.t27", + "specs/automation/token-wallet.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/agent-hive-group.t27", + "specs/automation/card-chat-night-watch.t27", + "specs/queen/dispatch.t27", + "specs/queen/hive.t27" +]; +pub const LESSON_TITLES : [27]str = [ + "Shaping an issue", + "A boundary of real files", + "One task, three kinds", + "Aging a label", + "The queue", + "Review first", + "A task's life", + "The ship flies home", + "A deploy mid-flight", + "Enough reviewers", + "Back off", + "The merge gate", + "The review jam", + "The Queen's move", + "Inherited red", + "A refused key", + "A key rests", + "The next key", + "No credit for keys", + "Twice the cost", + "Tokens that burn", + "Who is called", + "Three bots deep", + "Stop and mute", + "The night watch", + "The public board", + "The comb answers" +]; +pub const LESSON_GOALS : [27]str = [ + "What the Queen does with an issue that lacks sections, and why she stops after two tries.", + "Which proposed paths a task may reserve, and why one invented path stops the whole shape.", + "How an issue, a review and a job become one kind of task with seven states, and how one filter reads them.", + "How a labelled issue moves up with age, and why only by one level.", + "How the cap, aging and blockers order a queue of open issues.", + "Why the Queen closes the backlog while finished work waits for a review, and what still goes.", + "Which states a dispatched task passes through, and which events are legal in each.", + "Where the game draws a bee for each thing it is doing, and how a finished task ends on screen.", + "What a redeploy does to a bee, a review, a round, a job and a wait.", + "How many reviews must run at once for the reviewer to keep up with the bees.", + "Why a row that answers wait is visited less and less often, and how often at least.", + "When the merger may merge a pull request a reviewer bee approved.", + "Which of ten cases a stuck pull request is in, and why the earliest matching rule wins.", + "What the Queen does for each jam, and which jams she waits on.", + "When a red check on a pull request is inherited from master, and why a cancelled run says nothing.", + "How the Queen sorts a provider's refusal into transient, quota, auth or a bad request.", + "How long a refused key rests before a bee may use it again.", + "Which key a bee tries next, and when it moves to another pool or gives up.", + "What a contributor's lent keys earn, and which limits keep them safe.", + "How the project prices an operation and what a month of tokens buys.", + "When a batch of tokens burns, and which batch is spent first.", + "Which messages in the hive group wake an agent, from a person and from another agent's bot.", + "How the hive stops agents from answering each other forever.", + "What /stop and /mute do, and why neither can lose a person or a direct call.", + "What the night watch does in the morning with four card checks.", + "Which column the public board draws each claim in, and the case it gets wrong.", + "How the game shows that something happened to a task, and how often the map grows." +]; +pub const LESSON_TEXTS : [27]str = [ + "A bee can start only on an issue that says what to do and which files it may touch. The Queen holds every open issue to judge() of specs/policy/issue_shape.t27. One that is not ready she shapes: a model writes the missing sections, and the body goes back only if judge() then finds nothing missing. After SHAPE_ATTEMPT_LIMIT, 2 failed shapes, she stops and comments once with what is missing, and an epic is never shaped. shape_action in shaping.t27 decides, and a new try waits 360 minutes.", + "A task's boundary is a reservation: while a bee works, no other bee takes those files. So a wrong boundary is worse than a missing one, because it holds files the work does not own. path_accepted takes any file that exists, and a new file only when its folder exists, the issue cites it, and its language is one the only-t27 rule allows. shape_lands writes the shape back only when no section is missing, it names 1 to 12 paths, and none of them was refused.", + "The Queen runs three kinds of work: issues for bees, reviews of finished work, and jobs. Each kept its own codes, so no board could show them together. tasks.t27 gives them 7 shared states, numbered as the board's columns plus failed. A cancelled job shows as dropped, and so does a review row that was forgotten. task_matches is the filter the board, the game and the route all apply: a bit per kind, a bit per state, and 0 means any.", + "Without labels the Queen takes issues in GitHub's listing order. priority.t27 adds four levels, critical, high, normal and low; lower runs first, and an unlabelled issue counts as normal. A labelled issue gains one level after AGING_DAYS, 14 days, and at most MAX_AGING_STEPS, 1 level, so a low issue reaches normal on day 14 and stays there. High and critical do not age. effective_level computes the line the widget draws.", + "Labels inflate: if every issue is critical, none is. capped_level lets at most CRITICAL_CAP, 3, issues of a listing run as critical, taken in listing order, and the 4th runs as high. An issue with an open blocker is skipped, not waited on, so it cannot hold up the queue. outranks compares the effective level, then the real level, then the place in the listing, so an aged issue never beats a real one of its level. why names the reason for each place.", + "On 2026-10-18 the status page read 69 of 70 workers busy, and in one hour 102 dispatches, 56 finished and 11 reviewed. Bees started about nine for every verdict, and each new bee held files that the bees in review needed when they were sent back. review_first.t27 closes the backlog while the review queue is longer than REVIEW_QUEUE_MAX, 0. Work the review sent back and work the owner assigned still go: admission admits them at any queue length.", + "dispatch.t27 is the Queen's task lifecycle, the cycle that chooses, starts, ends, reviews and releases work. A task has 12 states and 16 events. next_state answers every pair, and 99 means the event is illegal in that state: a verdict cannot reach a running task, and a reap cannot reach an ended one. After a verdict a send-back waits for its floor and is released; even an accept is sent back if CI turns red under the ceiling.", + "In the game at app.t27.ai/game each bee is a ship over the issue it works on. ship_mode in hive.t27 picks the place: parked at the entrance before a runner claims it, orbiting its cell while it is heard from, hovering when it has gone quiet, and waiting at its repository's portal until its cell is placed. When the bee leaves the feed it flies home, and ship_alpha fades it over DEPART_MS, 2400 ms: at 1200 ms it is at 500 of 1000.", + "Every merge redeploys the Queen, and work is running when it does. drain.t27 sorts that work. A round, a job step and a wait row are rows the store keeps: the process leaves them and the next process carries on. A review is stopped, because a stop loses at most one review, which is redone. A bee is held, because what it has done lives in its conversation; at the cap, drain_cap_seconds, 1800 s today, it is handed back and its work is salvaged into its branch.", + "A review may take REVIEW_ROW_SECONDS, 300 s, so one slot clears at most 12 rows an hour. On 2026-10-09 bees finished about 58 an hour while 111 rows waited, and the fixed REVIEWER_CONCURRENCY of 4 tops out at 48. slots_needed(58) is 5. reviewer_concurrency takes the keys, their lanes and the free memory, never goes above 16 and never below 1 while anything waits, and keeps_up compares it with the finish rate.", + "Over 44 minutes on 2026-10-09 the reviewer made 941 row visits, 888 of them wait, from 22 rows whose branch it could not read; one row was visited 54 times. revisit_after_seconds now waits 30 s after one wait and doubles per wait, never past 900 s: 30, 60, 120, 240, 480, 900. due_again lets the row be taken only once that time has passed, and any other answer resets the count.", + "The reviewer bee approves, and the merger merges; merger_gate.t27 is the rule between them. check_passes lets a green check through and blocks any check still running. A red one passes only when it is not required, has concluded, and the bee named it as discounted, and with the ruleset unreadable every check counts as required. gate_open needs at least 1 posted check, 0 running and 0 blocking: zero checks is not zero failures.", + "A pull request can stall for many reasons at once: refused, pushed again, and red on a noise check. review-jam.t27 gives the Queen one answer. jam_case checks 9 facts in a fixed order and the earliest rule that matches wins, so a PR the Queen could not measure is never reported as free, and one that was refused and then pushed is not taken for noise. There are 10 cases, from none to unmeasured.", + "Each case has one action at the same position: no reviewer dispatches a reviewer, refused not fixed dispatches the coder, refused then pushed dispatches a re-review, and labeled not merged finishes the merge. jam_action maps any value above 9 to measure again, so an unknown case is never nothing to do. jam_is_queens_turn is false for 3 cases, none, draft or fresh, and stacked on an open PR: she waits on those.", + "A bee may approve a PR with a red check when that check is red on master too. On 2026-10-06 a PR merged red because master's run of that check was cancelled and was read as red. Now the merger reads master itself, newest run first: run_says counts only a completed failure or success, look_back passes over everything else and over runs older than the merge base, and discount_holds needs the bee's word and MASTER RED.", + "Bees call models with keys, and keys get refused. key_failover.t27 sorts each refusal with error_kind. The z.ai code in the body wins over the HTTP status, because z.ai sends a spent quota and a passing overload with the same 429: code 1305 is transient, 1113 is quota. No answer at all is transient. A quota or auth refusal closes the key, and only a bad request is charged to the issue.", + "A transient refusal rests the key COOL_BASE_SECONDS, 15 s, doubled per strike in a row and never more than COOL_CAP_SECONDS, 240 s: 15, 30, 60, 120, 240. A spent quota or a refused key closes it for KEY_BREAK_SECONDS, 1800 s. cooldown_seconds gives all of it. The rest is short so that 34 keys are never all resting through a one-minute burst like the one of 2026-10-17.", + "On 2026-10-17 a bee started on key 9 of 10 and died 7 s later while 33 other keys stood by: it got one key at dispatch and kept it. next_free now picks the next key after the last one that is neither tried nor resting. 1305 is an overload of the pool's model, so once POOL_OVERLOAD_KEYS, 3, keys said it within a minute, step moves the bee to the next pool. After 8 attempts the turn ends.", + "Anyone can lend the hive model keys through queen-contributor-keys.t27. A key is probed before it is enabled: at most 4 probes at once, a new probe waits out 60 s after the last, and an owner holds at most 100 keys. Lending earns nothing by itself: CREDIT_WALLET is false, so wallet_credit answers 0 for any number of keys. XP follows the work the Queen records.", + "Every dollar becomes tokens at one rate, TOKEN_MICRO_USD 5000: a token is half a cent. price_tokens sells an operation at twice what it costs, MARKUP_PCT 200, and rounds up so nothing sells below that: a cost of 12300 micro-dollars is 5 tokens. month_tokens turns $20 into 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12.", + "The owner's rule of 2026-10-01: all money goes into tokens, and tokens not spent within the month burn, so a person who paid makes content instead of saving tokens. Each batch lives MONTH_DAYS, 30 days, and days_left counts it down from the day it landed. On day 25 the package month from day 0 has 5 days left, the start gift from day 3 has 8, and a top-up from day 12 has 17. The spend order is soonest burn first.", + "Every agent has its own bot and its own topic in one forum group. A person calls an agent by its tag, by @all, by a reply, or by writing in its topic. Another agent's bot calls it only by a tag or a reply: a bot never triggers the whole hive and never wakes a topic's owner by merely writing there. addressed in agent-hive-group.t27 is that rule, one line long.", + "Telegram gives no loop protection between bots, so two agents could answer each other forever. Two numbers stop it. MAX_BOT_DEPTH is 3: a person, then A, B and C, then silence. BOT_REPLY_COOLDOWN_MS is 5000: a bot answers another bot at most once in 5 s. may_answer always lets a person through, and a bot only inside the chain and after the cooldown.", + "cooled says whether 5000 ms have passed since the last bot message. /stop silences bot-to-bot turns in a chat for STOP_WINDOW_MS, 10 minutes, and a person is still heard: that is may_answer_after_stop. /mute keeps an agent quiet in a topic for MUTE_MS, an hour, but it still answers its own tag: heard_while_muted. Both are clocks, so nobody has to switch the hive back on.", + "Every night four checks of the card chat run: the thread, the status door, the merges and the parity. night_verdict sums them up: any fail is a fail, else anything unmeasured is unmeasured, else it is a pass. night_report turns that into one act: a fail opens an issue or comments on the open one, a pass closes an open issue or stays silent, and an unmeasured night leaves everything as it is.", + "The board at app.t27.ai/game/kanban draws every dispatch in one of 6 columns. column_of reads the claim a dispatch holds on its issue: running is running, accepted is done, rejected and awaiting review are in review, and failed is dropped. dispatch.t27 records one finding as it is: once an issue is closed and the list was read, every finished claim is drawn as done, whatever its verdict.", + "The game's comb has a cell for each issue, and before hive.t27 an accepted task, a lost lease and a cancel looked the same as no news. cell_effect gives each of the 10 event kinds its paint: a start white, an accepting review gold, a send-back amber, a lost lease red, a cancel grey. flash_alpha fades it over FLASH_MS, 1800 ms. A new cell rebuilds the scene, so cells_due places new cells in batches at most every 300 s." +]; +pub const LESSON_TASKS : [27]str = [ + "Set two sections missing and step through the tries; then mark the author told and see which column the issue goes to.", + "Make the two red paths green by changing only facts that could be true; then leave a section missing and watch the gate.", + "Show only failed tasks of every kind; then find which job state shows as dropped.", + "Find the day a labelled low issue moves up; then remove the label and drag to day 60.", + "Make a fifth issue critical and see where it lands; then label the unlabelled issue low.", + "Drag the queue to 0 and back to 1; then find the candidates that are admitted at any length.", + "Walk a task from none to accept; then make CI red at the ceiling and find the one way out.", + "Mark the bee as not heard from and see where it moves; then drag the time past 2400 ms.", + "Drag the wait from 0 to the cap and watch the bee's row change; then cut the platform drain to 1000 s.", + "Find the fewest keys of 1 lane that keep up with 58 an hour; then cut the memory to 1000 MB.", + "Find the wait count where the pause stops growing; then set 4 waits and find the second the row is due.", + "Let the running check conclude green; then make the ruleset unreadable and watch the discounted red.", + "Clear facts one by one from the start state and name each case you pass; then find a PR that is in no jam.", + "Find the case whose action is wait for the base; then drag to 200 and read the action.", + "Make every run cancelled; then move the failure before the merge base and look at the gate.", + "Compare 429 with codes 1308 and 1312; then read what no answer at all is.", + "Find the strike count where the rest stops growing; then switch to quota and read the rest.", + "Mark key 2 as tried and see which key gets the ring; then set three keys that said 1305.", + "Drag the keys from 0 to 100 and watch the credit meter; then find the second a probe may start.", + "Find the costs that give exactly 1 token; then find the monthly price that buys 36 reels.", + "Find the day the package month burns; then find the last day any batch is left.", + "Find the two ways only a person can call an agent; then turn on all four and compare the senders.", + "Find the gap at which agent B falls silent; then explain why C is silent too.", + "Drag the /stop clock past 10 minutes; then tag the muted agent.", + "Make the night pass with the issue open; then make one check unmeasured.", + "Open the issue and see where the four finished claims go; then say what a closed issue hides.", + "Tap the review cell and switch the verdict; then find the moment the flash is gone." +]; + +; --- Claims -------------------------------------------------------------------------------- +test the_course_is_three_cubed { + assert LESSONS_PER_MODULE == 3; + assert MODULE_COUNT == 9; + assert LESSON_COUNT == 27; + assert MODULE_COUNT * LESSONS_PER_MODULE == LESSON_COUNT; + assert LESSON_COUNT == 3 * 3 * 3; +} + +test it_starts_with_a_shaped_issue_and_ends_on_the_comb { + assert MODULE_IDS[0] == "the-task"; + assert LESSON_WIDGETS[0] == "hive-shape"; + assert LESSON_SPECS[0] == "specs/queen/shaping.t27"; + assert MODULE_IDS[8] == "night-and-the-board"; + assert LESSON_WIDGETS[26] == "hive-cell"; + assert LESSON_SPECS[26] == "specs/queen/hive.t27"; +} + +test the_order_comes_before_the_bee_and_the_bee_before_its_review { + assert MODULE_IDS[1] == "who-goes-next"; + assert LESSON_SPECS[3] == "specs/queen/priority.t27"; + assert MODULE_IDS[2] == "a-bees-life"; + assert LESSON_SPECS[6] == "specs/queen/dispatch.t27"; + assert MODULE_IDS[3] == "the-reviewer"; + assert LESSON_WIDGETS[11] == "hive-gate"; +} + +test the_jam_runs_the_review_jam_spec { + assert MODULE_IDS[4] == "the-jam"; + assert LESSON_SPECS[12] == "specs/automation/review-jam.t27"; + assert LESSON_SPECS[13] == "specs/automation/review-jam.t27"; + assert LESSON_WIDGETS[14] == "hive-master-red"; +} + +test keys_then_money_then_agents { + assert MODULE_IDS[5] == "keys"; + assert LESSON_SPECS[17] == "specs/queen/key_failover.t27"; + assert MODULE_IDS[6] == "credit-and-money"; + assert LESSON_SPECS[19] == "specs/automation/token-wallet.t27"; + assert MODULE_IDS[7] == "agents-together"; + assert LESSON_SPECS[22] == "specs/automation/agent-hive-group.t27"; +} + +test the_course_sends_nothing { + assert SENDS_NOTHING == true; + assert LOCALES[0] == "en"; + assert LOCALES[1] == "ru"; +} diff --git a/apps/website/specs/widgets/gallery.t27 b/apps/website/specs/widgets/gallery.t27 index 17117ac2a9..fb07268ccf 100644 --- a/apps/website/specs/widgets/gallery.t27 +++ b/apps/website/specs/widgets/gallery.t27 @@ -78,15 +78,15 @@ pub const TOOL_NOSCRIPT : str = "This widget runs in your browser and needs Java ; Parallel arrays, WIDGET_COUNT long. WIDGET_TITLES is empty wherever the page carries an ; og:title, which is every kind but tab: the page is the one place the words live. A tool's ; WIDGET_HOOKS entry is empty too: its hook is the page's t27:hook, written from its own spec. -pub const WIDGET_COUNT : u16 = 279; -pub const WIDGET_IDS : [279]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "bit-diff", "build-receipt", "cost-curves", "gf16-calc", "idcode", "lut-explain", "pin-map", "placement-map", "resource-bars", "slack-waterfall", "test-waves", "synth-smells", "bit-autopsy", "fasm-skyline", "vcd-wrapped", "fits-on", "stat-card", "lut-depth", "fsm-sketch", "lut-treemap", "t27-cell", "t27-vs-nvfp4", "uart-bucket", "tri-fpga-specs", "tri-fpga-tools", "tri-fpga-wire", "tri-fpga-batch", "tri-fpga-status", "tri-fpga-tmpcheck", "tri-fpga-keycheck", "tri-fpga-selftest", "tri-x7-refs", "tri-x7-audit", "t27c-clock-domain", "t27c-clocks", "t27c-wukong-v1", "t27c-cts", "t27c-mmcm", "t27c-mmcm-mutate", "t27c-timing", "t27c-reset-sync", "t27c-reset-sync-gen", "t27c-mtbf", "t27c-clock-domain-gen", "t27c-fifo", "t27c-fifo-mutate", "t27c-fifo-flags", "t27c-emitter-xdc", "t27c-fpga-rxcap", "t27-tri-competitors", "t27-tri-discard", "tri-fpga-txhold", "tri-fpga-rxcap", "tri-fpga-rxer", "tri-fpga-steps", "tri-fpga-ioclients", "tri-fpga-claim", "tri-fpga-release", "tri-fpga-next", "t27c-memory", "t27c-spi-tb", "t27c-check-uart", "t27c-check-spi", "t27c-check-apb", "t27c-check-axi4", "t27c-check-bridge", "t27c-check-memory", "t27c-verilog-uart", "t27c-verilog-spi", "t27c-verilog-apb", "t27c-verilog-axi4", "t27c-verilog-bridge", "t27c-verilog-top", "t27c-verilog-uart-tb", "t27c-verilog-apb-tb", "t27c-verilog-axi-tb", "t27c-verilog-mem-tb", "t27c-verilog-integration-tb", "t27c-verilog-top-tb", "t27c-verilog-spi-tb", "t27c-hir-uart", "t27c-hir-apb", "t27c-hir-axi4", "t27c-eth-crc", "t27c-rgmii", "jtag-verdict", "line-bounce", "eye-isi", "ecc-secded", "carry-chain", "prefix-adder", "csa-tree", "shift-add", "booth-radix4", "dsp-fit", "q-format", "round-sat", "sqnr", "cordic", "cordic-vector", "sine-table", "alias-fold", "nco", "dft-bin", "fir-response", "fir-design", "fir-cost", "decimator", "cic-growth", "polyphase", "fft-butterfly", "bit-reverse", "fft-scaling", "barker-corr", "fir-fit", "fir-on-board", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "test-touch", "t27c-stages", "t27c-e8m0", "play-e8m0", "t27c-ocp-mx", "play-gfternary", "t27c-gfternary", "play-golden-sieve", "t27c-golden-sieve", "play-tnf17", "t27c-tnf17", "t27c-activation-quantizer", "t27c-bitnet-majority", "t27c-bitnet-neuron-nchunk", "t27c-comb-ternary-dot", "t27c-stream-ternary-mac", "t27c-bitnet-mlp", "t27c-gft-smul", "t27c-gft-sadd", "t27c-gft-signed-mac", "t27c-gft-relu", "t27c-gft-exp2", "t27c-gft-argmax4", "t27c-gft-nll", "t27c-gft-sgd-step", "t27c-gft-xornet", "basics-what-a-spec-is", "basics-the-module-line", "basics-comments-and-prose", "basics-constants", "basics-whole-numbers", "basics-true-false-and-text", "basics-arrays", "basics-trits", "basics-expressions", "basics-a-test-block", "basics-many-tests", "basics-functions", "basics-local-names", "basics-if-and-else", "basics-loops", "basics-pub-or-private", "basics-structs", "basics-enums", "basics-use-other-modules", "basics-invariants", "basics-gen-ts", "basics-seven-backends", "basics-stages-of-t27c", "basics-tri-commands", "basics-reading-errors", "basics-a-small-program", "basics-where-next", "golden-split", "golden-phi-ratio", "golden-lucas", "golden-split-optimality", "golden-radix-economy", "golden-phi-in-gf16", "golden-gf4", "golden-gf6", "golden-gf8", "golden-gf10", "golden-gf12", "golden-gf14", "golden-gf16", "golden-dot-two", "golden-gf24", "golden-gf32", "golden-gf48", "golden-gf64", "golden-gf96", "golden-gf128", "golden-gf256", "golden-gf512", "golden-gf1024", "golden-gft8", "golden-gft16", "golden-gft32", "golden-decode", "t27-tri-vectors", "t27-tri-census", "t27-tri-seals", "t27-tri-prose", "t27-tri-lean", "tri-selftest", "play-terminal-gates", "tc-why-three", "tc-balanced-count", "tc-flip-and-cut", "tc-kleene-gates", "tc-consensus-any", "tc-sum-and-carry", "tc-half-adder", "tc-full-adder", "tc-ripple-carry", "tc-times-a-trit", "tc-long-multiply", "tc-ternary-mac", "tc-two-bits", "tc-five-in-a-byte", "tc-trit-word", "tc-instruction-word", "tc-opcode-map", "tc-immediate", "tc-ternary-alu", "tc-three-way-jump", "tc-run-a-program", "tc-compare", "tc-thirds", "tc-sort-three", "tc-one-neuron", "tc-detector", "tc-tiny-layer", "nn-relu", "nn-sigmoid", "nn-tanh", "nn-gelu", "nn-silu", "nn-elu", "nn-softmax", "nn-embedding", "nn-dropout", "nn-conv", "nn-maxpool", "nn-batchnorm", "nn-mse", "nn-bce", "nn-huber", "nn-sgd", "nn-rmsprop", "nn-adam", "nn-backprop", "nn-xor", "nn-lamb", "nn-rnn", "nn-lstm", "nn-attention", "nn-positions", "nn-encoder", "nn-ppo", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts", "race-the-bee", "gatle", "tri-blog-list", "tri-game-selftest"]; -pub const WIDGET_KINDS : [279]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "player", "player", "player", "cast", "cast", "tool", "tool", "cast", "player", "cast", "player", "cast", "player", "cast", "player", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tab", "cast", "cast", "cast", "tool", "tool", "cast", "cast"]; -pub const WIDGET_CATEGORIES : [279]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game", "game", "game", "game", "game"]; -pub const WIDGET_PAGES : [279]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "widgets/bit-diff/", "widgets/build-receipt/", "widgets/cost-curves/", "widgets/gf16-calc/", "widgets/idcode/", "widgets/lut-explain/", "widgets/pin-map/", "widgets/placement-map/", "widgets/resource-bars/", "widgets/slack-waterfall/", "widgets/test-waves/", "widgets/synth-smells/", "widgets/bit-autopsy/", "widgets/fasm-skyline/", "widgets/vcd-wrapped/", "widgets/fits-on/", "widgets/stat-card/", "widgets/lut-depth/", "widgets/fsm-sketch/", "widgets/lut-treemap/", "widgets/t27-cell/", "widgets/t27-vs-nvfp4/", "widgets/uart-bucket/", "term/tri-fpga-specs/", "term/tri-fpga-tools/", "term/tri-fpga-wire/", "term/tri-fpga-batch/", "term/tri-fpga-status/", "term/tri-fpga-tmpcheck/", "term/tri-fpga-keycheck/", "term/tri-fpga-selftest/", "term/tri-x7-refs/", "term/tri-x7-audit/", "term/t27c-clock-domain/", "term/t27c-clocks/", "term/t27c-wukong-v1/", "term/t27c-cts/", "term/t27c-mmcm/", "term/t27c-mmcm-mutate/", "term/t27c-timing/", "term/t27c-reset-sync/", "term/t27c-reset-sync-gen/", "term/t27c-mtbf/", "term/t27c-clock-domain-gen/", "term/t27c-fifo/", "term/t27c-fifo-mutate/", "term/t27c-fifo-flags/", "term/t27c-emitter-xdc/", "term/t27c-fpga-rxcap/", "term/t27-tri-competitors/", "term/t27-tri-discard/", "term/tri-fpga-txhold/", "term/tri-fpga-rxcap/", "term/tri-fpga-rxer/", "term/tri-fpga-steps/", "term/tri-fpga-ioclients/", "term/tri-fpga-claim/", "term/tri-fpga-release/", "term/tri-fpga-next/", "term/t27c-memory/", "term/t27c-spi-tb/", "term/t27c-check-uart/", "term/t27c-check-spi/", "term/t27c-check-apb/", "term/t27c-check-axi4/", "term/t27c-check-bridge/", "term/t27c-check-memory/", "term/t27c-verilog-uart/", "term/t27c-verilog-spi/", "term/t27c-verilog-apb/", "term/t27c-verilog-axi4/", "term/t27c-verilog-bridge/", "term/t27c-verilog-top/", "term/t27c-verilog-uart-tb/", "term/t27c-verilog-apb-tb/", "term/t27c-verilog-axi-tb/", "term/t27c-verilog-mem-tb/", "term/t27c-verilog-integration-tb/", "term/t27c-verilog-top-tb/", "term/t27c-verilog-spi-tb/", "term/t27c-hir-uart/", "term/t27c-hir-apb/", "term/t27c-hir-axi4/", "term/t27c-eth-crc/", "term/t27c-rgmii/", "widgets/jtag-verdict/", "widgets/line-bounce/", "widgets/eye-isi/", "widgets/ecc-secded/", "widgets/carry-chain/", "widgets/prefix-adder/", "widgets/csa-tree/", "widgets/shift-add/", "widgets/booth-radix4/", "widgets/dsp-fit/", "widgets/q-format/", "widgets/round-sat/", "widgets/sqnr/", "widgets/cordic/", "widgets/cordic-vector/", "widgets/sine-table/", "widgets/alias-fold/", "widgets/nco/", "widgets/dft-bin/", "widgets/fir-response/", "widgets/fir-design/", "widgets/fir-cost/", "widgets/decimator/", "widgets/cic-growth/", "widgets/polyphase/", "widgets/fft-butterfly/", "widgets/bit-reverse/", "widgets/fft-scaling/", "widgets/barker-corr/", "widgets/fir-fit/", "widgets/fir-on-board/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "widgets/test-touch/", "widgets/t27c-stages/", "term/t27c-e8m0/", "play/e8m0/", "term/t27c-ocp-mx/", "play/gfternary/", "term/t27c-gfternary/", "play/golden-sieve/", "term/t27c-golden-sieve/", "play/tnf17/", "term/t27c-tnf17/", "term/t27c-activation-quantizer/", "term/t27c-bitnet-majority/", "term/t27c-bitnet-neuron-nchunk/", "term/t27c-comb-ternary-dot/", "term/t27c-stream-ternary-mac/", "term/t27c-bitnet-mlp/", "term/t27c-gft-smul/", "term/t27c-gft-sadd/", "term/t27c-gft-signed-mac/", "term/t27c-gft-relu/", "term/t27c-gft-exp2/", "term/t27c-gft-argmax4/", "term/t27c-gft-nll/", "term/t27c-gft-sgd-step/", "term/t27c-gft-xornet/", "widgets/basics-what-a-spec-is/", "widgets/basics-the-module-line/", "widgets/basics-comments-and-prose/", "widgets/basics-constants/", "widgets/basics-whole-numbers/", "widgets/basics-true-false-and-text/", "widgets/basics-arrays/", "widgets/basics-trits/", "widgets/basics-expressions/", "widgets/basics-a-test-block/", "widgets/basics-many-tests/", "widgets/basics-functions/", "widgets/basics-local-names/", "widgets/basics-if-and-else/", "widgets/basics-loops/", "widgets/basics-pub-or-private/", "widgets/basics-structs/", "widgets/basics-enums/", "widgets/basics-use-other-modules/", "widgets/basics-invariants/", "widgets/basics-gen-ts/", "widgets/basics-seven-backends/", "widgets/basics-stages-of-t27c/", "widgets/basics-tri-commands/", "widgets/basics-reading-errors/", "widgets/basics-a-small-program/", "widgets/basics-where-next/", "widgets/golden-split/", "widgets/golden-phi-ratio/", "widgets/golden-lucas/", "widgets/golden-split-optimality/", "widgets/golden-radix-economy/", "widgets/golden-phi-in-gf16/", "widgets/golden-gf4/", "widgets/golden-gf6/", "widgets/golden-gf8/", "widgets/golden-gf10/", "widgets/golden-gf12/", "widgets/golden-gf14/", "widgets/golden-gf16/", "widgets/golden-dot-two/", "widgets/golden-gf24/", "widgets/golden-gf32/", "widgets/golden-gf48/", "widgets/golden-gf64/", "widgets/golden-gf96/", "widgets/golden-gf128/", "widgets/golden-gf256/", "widgets/golden-gf512/", "widgets/golden-gf1024/", "widgets/golden-gft8/", "widgets/golden-gft16/", "widgets/golden-gft32/", "widgets/golden-decode/", "term/t27-tri-vectors/", "term/t27-tri-census/", "term/t27-tri-seals/", "term/t27-tri-prose/", "term/t27-tri-lean/", "term/tri-selftest/", "term/play-terminal-gates/", "widgets/tc-why-three/", "widgets/tc-balanced-count/", "widgets/tc-flip-and-cut/", "widgets/tc-kleene-gates/", "widgets/tc-consensus-any/", "widgets/tc-sum-and-carry/", "widgets/tc-half-adder/", "widgets/tc-full-adder/", "widgets/tc-ripple-carry/", "widgets/tc-times-a-trit/", "widgets/tc-long-multiply/", "widgets/tc-ternary-mac/", "widgets/tc-two-bits/", "widgets/tc-five-in-a-byte/", "widgets/tc-trit-word/", "widgets/tc-instruction-word/", "widgets/tc-opcode-map/", "widgets/tc-immediate/", "widgets/tc-ternary-alu/", "widgets/tc-three-way-jump/", "widgets/tc-run-a-program/", "widgets/tc-compare/", "widgets/tc-thirds/", "widgets/tc-sort-three/", "widgets/tc-one-neuron/", "widgets/tc-detector/", "widgets/tc-tiny-layer/", "widgets/nn-relu/", "widgets/nn-sigmoid/", "widgets/nn-tanh/", "widgets/nn-gelu/", "widgets/nn-silu/", "widgets/nn-elu/", "widgets/nn-softmax/", "widgets/nn-embedding/", "widgets/nn-dropout/", "widgets/nn-conv/", "widgets/nn-maxpool/", "widgets/nn-batchnorm/", "widgets/nn-mse/", "widgets/nn-bce/", "widgets/nn-huber/", "widgets/nn-sgd/", "widgets/nn-rmsprop/", "widgets/nn-adam/", "widgets/nn-backprop/", "widgets/nn-xor/", "widgets/nn-lamb/", "widgets/nn-rnn/", "widgets/nn-lstm/", "widgets/nn-attention/", "widgets/nn-positions/", "widgets/nn-encoder/", "widgets/nn-ppo/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/", "widgets/race-the-bee/", "widgets/gatle/", "term/tri-blog-list/", "term/tri-game-selftest/"]; -pub const WIDGET_TITLES : [279]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", "", "", "", "", ""]; -pub const WIDGET_LINES : [279]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", "", "", "", "", ""]; +pub const WIDGET_COUNT : u16 = 306; +pub const WIDGET_IDS : [306]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "bit-diff", "build-receipt", "cost-curves", "gf16-calc", "idcode", "lut-explain", "pin-map", "placement-map", "resource-bars", "slack-waterfall", "test-waves", "synth-smells", "bit-autopsy", "fasm-skyline", "vcd-wrapped", "fits-on", "stat-card", "lut-depth", "fsm-sketch", "lut-treemap", "t27-cell", "t27-vs-nvfp4", "uart-bucket", "tri-fpga-specs", "tri-fpga-tools", "tri-fpga-wire", "tri-fpga-batch", "tri-fpga-status", "tri-fpga-tmpcheck", "tri-fpga-keycheck", "tri-fpga-selftest", "tri-x7-refs", "tri-x7-audit", "t27c-clock-domain", "t27c-clocks", "t27c-wukong-v1", "t27c-cts", "t27c-mmcm", "t27c-mmcm-mutate", "t27c-timing", "t27c-reset-sync", "t27c-reset-sync-gen", "t27c-mtbf", "t27c-clock-domain-gen", "t27c-fifo", "t27c-fifo-mutate", "t27c-fifo-flags", "t27c-emitter-xdc", "t27c-fpga-rxcap", "t27-tri-competitors", "t27-tri-discard", "tri-fpga-txhold", "tri-fpga-rxcap", "tri-fpga-rxer", "tri-fpga-steps", "tri-fpga-ioclients", "tri-fpga-claim", "tri-fpga-release", "tri-fpga-next", "t27c-memory", "t27c-spi-tb", "t27c-check-uart", "t27c-check-spi", "t27c-check-apb", "t27c-check-axi4", "t27c-check-bridge", "t27c-check-memory", "t27c-verilog-uart", "t27c-verilog-spi", "t27c-verilog-apb", "t27c-verilog-axi4", "t27c-verilog-bridge", "t27c-verilog-top", "t27c-verilog-uart-tb", "t27c-verilog-apb-tb", "t27c-verilog-axi-tb", "t27c-verilog-mem-tb", "t27c-verilog-integration-tb", "t27c-verilog-top-tb", "t27c-verilog-spi-tb", "t27c-hir-uart", "t27c-hir-apb", "t27c-hir-axi4", "t27c-eth-crc", "t27c-rgmii", "jtag-verdict", "line-bounce", "eye-isi", "ecc-secded", "carry-chain", "prefix-adder", "csa-tree", "shift-add", "booth-radix4", "dsp-fit", "q-format", "round-sat", "sqnr", "cordic", "cordic-vector", "sine-table", "alias-fold", "nco", "dft-bin", "fir-response", "fir-design", "fir-cost", "decimator", "cic-growth", "polyphase", "fft-butterfly", "bit-reverse", "fft-scaling", "barker-corr", "fir-fit", "fir-on-board", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "test-touch", "t27c-stages", "t27c-e8m0", "play-e8m0", "t27c-ocp-mx", "play-gfternary", "t27c-gfternary", "play-golden-sieve", "t27c-golden-sieve", "play-tnf17", "t27c-tnf17", "t27c-activation-quantizer", "t27c-bitnet-majority", "t27c-bitnet-neuron-nchunk", "t27c-comb-ternary-dot", "t27c-stream-ternary-mac", "t27c-bitnet-mlp", "t27c-gft-smul", "t27c-gft-sadd", "t27c-gft-signed-mac", "t27c-gft-relu", "t27c-gft-exp2", "t27c-gft-argmax4", "t27c-gft-nll", "t27c-gft-sgd-step", "t27c-gft-xornet", "basics-what-a-spec-is", "basics-the-module-line", "basics-comments-and-prose", "basics-constants", "basics-whole-numbers", "basics-true-false-and-text", "basics-arrays", "basics-trits", "basics-expressions", "basics-a-test-block", "basics-many-tests", "basics-functions", "basics-local-names", "basics-if-and-else", "basics-loops", "basics-pub-or-private", "basics-structs", "basics-enums", "basics-use-other-modules", "basics-invariants", "basics-gen-ts", "basics-seven-backends", "basics-stages-of-t27c", "basics-tri-commands", "basics-reading-errors", "basics-a-small-program", "basics-where-next", "golden-split", "golden-phi-ratio", "golden-lucas", "golden-split-optimality", "golden-radix-economy", "golden-phi-in-gf16", "golden-gf4", "golden-gf6", "golden-gf8", "golden-gf10", "golden-gf12", "golden-gf14", "golden-gf16", "golden-dot-two", "golden-gf24", "golden-gf32", "golden-gf48", "golden-gf64", "golden-gf96", "golden-gf128", "golden-gf256", "golden-gf512", "golden-gf1024", "golden-gft8", "golden-gft16", "golden-gft32", "golden-decode", "t27-tri-vectors", "t27-tri-census", "t27-tri-seals", "t27-tri-prose", "t27-tri-lean", "tri-selftest", "play-terminal-gates", "tc-why-three", "tc-balanced-count", "tc-flip-and-cut", "tc-kleene-gates", "tc-consensus-any", "tc-sum-and-carry", "tc-half-adder", "tc-full-adder", "tc-ripple-carry", "tc-times-a-trit", "tc-long-multiply", "tc-ternary-mac", "tc-two-bits", "tc-five-in-a-byte", "tc-trit-word", "tc-instruction-word", "tc-opcode-map", "tc-immediate", "tc-ternary-alu", "tc-three-way-jump", "tc-run-a-program", "tc-compare", "tc-thirds", "tc-sort-three", "tc-one-neuron", "tc-detector", "tc-tiny-layer", "nn-relu", "nn-sigmoid", "nn-tanh", "nn-gelu", "nn-silu", "nn-elu", "nn-softmax", "nn-embedding", "nn-dropout", "nn-conv", "nn-maxpool", "nn-batchnorm", "nn-mse", "nn-bce", "nn-huber", "nn-sgd", "nn-rmsprop", "nn-adam", "nn-backprop", "nn-xor", "nn-lamb", "nn-rnn", "nn-lstm", "nn-attention", "nn-positions", "nn-encoder", "nn-ppo", "hive-shape", "hive-boundary", "hive-task-filter", "hive-aging", "hive-rank", "hive-review-first", "hive-lifecycle", "hive-ship", "hive-drain", "hive-reviewers", "hive-revisit", "hive-gate", "hive-jam", "hive-jam-action", "hive-master-red", "hive-key-error", "hive-key-rest", "hive-next-key", "hive-credit", "hive-price", "hive-burn", "hive-called", "hive-chain", "hive-cooldown", "hive-night", "hive-column", "hive-cell", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts", "race-the-bee", "gatle", "tri-blog-list", "tri-game-selftest"]; +pub const WIDGET_KINDS : [306]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "player", "player", "player", "cast", "cast", "tool", "tool", "cast", "player", "cast", "player", "cast", "player", "cast", "player", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tool", "tab", "cast", "cast", "cast", "tool", "tool", "cast", "cast"]; +pub const WIDGET_CATEGORIES : [306]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game", "game"]; +pub const WIDGET_PAGES : [306]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "widgets/bit-diff/", "widgets/build-receipt/", "widgets/cost-curves/", "widgets/gf16-calc/", "widgets/idcode/", "widgets/lut-explain/", "widgets/pin-map/", "widgets/placement-map/", "widgets/resource-bars/", "widgets/slack-waterfall/", "widgets/test-waves/", "widgets/synth-smells/", "widgets/bit-autopsy/", "widgets/fasm-skyline/", "widgets/vcd-wrapped/", "widgets/fits-on/", "widgets/stat-card/", "widgets/lut-depth/", "widgets/fsm-sketch/", "widgets/lut-treemap/", "widgets/t27-cell/", "widgets/t27-vs-nvfp4/", "widgets/uart-bucket/", "term/tri-fpga-specs/", "term/tri-fpga-tools/", "term/tri-fpga-wire/", "term/tri-fpga-batch/", "term/tri-fpga-status/", "term/tri-fpga-tmpcheck/", "term/tri-fpga-keycheck/", "term/tri-fpga-selftest/", "term/tri-x7-refs/", "term/tri-x7-audit/", "term/t27c-clock-domain/", "term/t27c-clocks/", "term/t27c-wukong-v1/", "term/t27c-cts/", "term/t27c-mmcm/", "term/t27c-mmcm-mutate/", "term/t27c-timing/", "term/t27c-reset-sync/", "term/t27c-reset-sync-gen/", "term/t27c-mtbf/", "term/t27c-clock-domain-gen/", "term/t27c-fifo/", "term/t27c-fifo-mutate/", "term/t27c-fifo-flags/", "term/t27c-emitter-xdc/", "term/t27c-fpga-rxcap/", "term/t27-tri-competitors/", "term/t27-tri-discard/", "term/tri-fpga-txhold/", "term/tri-fpga-rxcap/", "term/tri-fpga-rxer/", "term/tri-fpga-steps/", "term/tri-fpga-ioclients/", "term/tri-fpga-claim/", "term/tri-fpga-release/", "term/tri-fpga-next/", "term/t27c-memory/", "term/t27c-spi-tb/", "term/t27c-check-uart/", "term/t27c-check-spi/", "term/t27c-check-apb/", "term/t27c-check-axi4/", "term/t27c-check-bridge/", "term/t27c-check-memory/", "term/t27c-verilog-uart/", "term/t27c-verilog-spi/", "term/t27c-verilog-apb/", "term/t27c-verilog-axi4/", "term/t27c-verilog-bridge/", "term/t27c-verilog-top/", "term/t27c-verilog-uart-tb/", "term/t27c-verilog-apb-tb/", "term/t27c-verilog-axi-tb/", "term/t27c-verilog-mem-tb/", "term/t27c-verilog-integration-tb/", "term/t27c-verilog-top-tb/", "term/t27c-verilog-spi-tb/", "term/t27c-hir-uart/", "term/t27c-hir-apb/", "term/t27c-hir-axi4/", "term/t27c-eth-crc/", "term/t27c-rgmii/", "widgets/jtag-verdict/", "widgets/line-bounce/", "widgets/eye-isi/", "widgets/ecc-secded/", "widgets/carry-chain/", "widgets/prefix-adder/", "widgets/csa-tree/", "widgets/shift-add/", "widgets/booth-radix4/", "widgets/dsp-fit/", "widgets/q-format/", "widgets/round-sat/", "widgets/sqnr/", "widgets/cordic/", "widgets/cordic-vector/", "widgets/sine-table/", "widgets/alias-fold/", "widgets/nco/", "widgets/dft-bin/", "widgets/fir-response/", "widgets/fir-design/", "widgets/fir-cost/", "widgets/decimator/", "widgets/cic-growth/", "widgets/polyphase/", "widgets/fft-butterfly/", "widgets/bit-reverse/", "widgets/fft-scaling/", "widgets/barker-corr/", "widgets/fir-fit/", "widgets/fir-on-board/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "widgets/test-touch/", "widgets/t27c-stages/", "term/t27c-e8m0/", "play/e8m0/", "term/t27c-ocp-mx/", "play/gfternary/", "term/t27c-gfternary/", "play/golden-sieve/", "term/t27c-golden-sieve/", "play/tnf17/", "term/t27c-tnf17/", "term/t27c-activation-quantizer/", "term/t27c-bitnet-majority/", "term/t27c-bitnet-neuron-nchunk/", "term/t27c-comb-ternary-dot/", "term/t27c-stream-ternary-mac/", "term/t27c-bitnet-mlp/", "term/t27c-gft-smul/", "term/t27c-gft-sadd/", "term/t27c-gft-signed-mac/", "term/t27c-gft-relu/", "term/t27c-gft-exp2/", "term/t27c-gft-argmax4/", "term/t27c-gft-nll/", "term/t27c-gft-sgd-step/", "term/t27c-gft-xornet/", "widgets/basics-what-a-spec-is/", "widgets/basics-the-module-line/", "widgets/basics-comments-and-prose/", "widgets/basics-constants/", "widgets/basics-whole-numbers/", "widgets/basics-true-false-and-text/", "widgets/basics-arrays/", "widgets/basics-trits/", "widgets/basics-expressions/", "widgets/basics-a-test-block/", "widgets/basics-many-tests/", "widgets/basics-functions/", "widgets/basics-local-names/", "widgets/basics-if-and-else/", "widgets/basics-loops/", "widgets/basics-pub-or-private/", "widgets/basics-structs/", "widgets/basics-enums/", "widgets/basics-use-other-modules/", "widgets/basics-invariants/", "widgets/basics-gen-ts/", "widgets/basics-seven-backends/", "widgets/basics-stages-of-t27c/", "widgets/basics-tri-commands/", "widgets/basics-reading-errors/", "widgets/basics-a-small-program/", "widgets/basics-where-next/", "widgets/golden-split/", "widgets/golden-phi-ratio/", "widgets/golden-lucas/", "widgets/golden-split-optimality/", "widgets/golden-radix-economy/", "widgets/golden-phi-in-gf16/", "widgets/golden-gf4/", "widgets/golden-gf6/", "widgets/golden-gf8/", "widgets/golden-gf10/", "widgets/golden-gf12/", "widgets/golden-gf14/", "widgets/golden-gf16/", "widgets/golden-dot-two/", "widgets/golden-gf24/", "widgets/golden-gf32/", "widgets/golden-gf48/", "widgets/golden-gf64/", "widgets/golden-gf96/", "widgets/golden-gf128/", "widgets/golden-gf256/", "widgets/golden-gf512/", "widgets/golden-gf1024/", "widgets/golden-gft8/", "widgets/golden-gft16/", "widgets/golden-gft32/", "widgets/golden-decode/", "term/t27-tri-vectors/", "term/t27-tri-census/", "term/t27-tri-seals/", "term/t27-tri-prose/", "term/t27-tri-lean/", "term/tri-selftest/", "term/play-terminal-gates/", "widgets/tc-why-three/", "widgets/tc-balanced-count/", "widgets/tc-flip-and-cut/", "widgets/tc-kleene-gates/", "widgets/tc-consensus-any/", "widgets/tc-sum-and-carry/", "widgets/tc-half-adder/", "widgets/tc-full-adder/", "widgets/tc-ripple-carry/", "widgets/tc-times-a-trit/", "widgets/tc-long-multiply/", "widgets/tc-ternary-mac/", "widgets/tc-two-bits/", "widgets/tc-five-in-a-byte/", "widgets/tc-trit-word/", "widgets/tc-instruction-word/", "widgets/tc-opcode-map/", "widgets/tc-immediate/", "widgets/tc-ternary-alu/", "widgets/tc-three-way-jump/", "widgets/tc-run-a-program/", "widgets/tc-compare/", "widgets/tc-thirds/", "widgets/tc-sort-three/", "widgets/tc-one-neuron/", "widgets/tc-detector/", "widgets/tc-tiny-layer/", "widgets/nn-relu/", "widgets/nn-sigmoid/", "widgets/nn-tanh/", "widgets/nn-gelu/", "widgets/nn-silu/", "widgets/nn-elu/", "widgets/nn-softmax/", "widgets/nn-embedding/", "widgets/nn-dropout/", "widgets/nn-conv/", "widgets/nn-maxpool/", "widgets/nn-batchnorm/", "widgets/nn-mse/", "widgets/nn-bce/", "widgets/nn-huber/", "widgets/nn-sgd/", "widgets/nn-rmsprop/", "widgets/nn-adam/", "widgets/nn-backprop/", "widgets/nn-xor/", "widgets/nn-lamb/", "widgets/nn-rnn/", "widgets/nn-lstm/", "widgets/nn-attention/", "widgets/nn-positions/", "widgets/nn-encoder/", "widgets/nn-ppo/", "widgets/hive-shape/", "widgets/hive-boundary/", "widgets/hive-task-filter/", "widgets/hive-aging/", "widgets/hive-rank/", "widgets/hive-review-first/", "widgets/hive-lifecycle/", "widgets/hive-ship/", "widgets/hive-drain/", "widgets/hive-reviewers/", "widgets/hive-revisit/", "widgets/hive-gate/", "widgets/hive-jam/", "widgets/hive-jam-action/", "widgets/hive-master-red/", "widgets/hive-key-error/", "widgets/hive-key-rest/", "widgets/hive-next-key/", "widgets/hive-credit/", "widgets/hive-price/", "widgets/hive-burn/", "widgets/hive-called/", "widgets/hive-chain/", "widgets/hive-cooldown/", "widgets/hive-night/", "widgets/hive-column/", "widgets/hive-cell/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/", "widgets/race-the-bee/", "widgets/gatle/", "term/tri-blog-list/", "term/tri-game-selftest/"]; +pub const WIDGET_TITLES : [306]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", "", "", "", "", ""]; +pub const WIDGET_LINES : [306]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", "", "", "", "", ""]; ; Why a person would pass this one on: the reason the widget is in the gallery at all. -pub const WIDGET_HOOKS : [279]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "17 of 17 conformance specs compile, their test blocks pass, and no generated file has drifted.", "14 of 14 bench tools pass their own self-tests in one sweep: decode 18/18, repin 17/17, firelog 10/10, tmpcheck 9/9, stamps 9/9, keep 9/9.", "Requested 115,200 puts 115,385 on the wire (+0.16 %), 921,600 puts 923,077, and 3,000,000 is exact.", "26 checks, no board, no RTL: every receipt returns, 11 loss and fault cases recover, and a lie stops the run.", "One command reads the bench: cables, locks, and the last board runs: three x7-board runs PASS 51840/51840.", "112 files under /tmp pinned by sha256 in specs: every one has a kept copy, 0 lost, 0 unlocated.", "84 board and model logs scanned for the TRI-NET key: 0 hits, 0 logs of 1 MB or more.", "16 spec-named scripts run their own --self-test, 0 bad; 8 without the flag are listed, not skipped silently.", "The part table in far.t27 matches prjxray-db part.json, all 6 reference bitstreams present, bitwalk reads all 6 in 84.4 ms.", "One command re-runs every sweep: CRC 12/12, ECC 32520/32520 frames, 82/82 pins, 4224/4224 segbits, gold 5/5. audit: CLEAN.", "The CDC spec through the real compiler: 12 tests pass natively and the header says what the model does and does not claim.", "The tri clocks card as a spec: constants a tool page reads, and a native test-report that says so plainly -- 0 tests, because it is not a test suite.", "The board itself as a spec: 15 tests pass natively and the part line names the xc7a200tfgg676 the Wukong carries.", "Clock-tree synthesis in numbers: 15 tests pass natively, and the header pins the Artix-7 teaching delays the tree arithmetic uses.", "The MMCM model through the real compiler: VCO window, M/D/O ranges, fractional multiply in eighths, the 1/56 phase step, 16 tests natively.", "Widen the VCO window with one sed and exactly one test objects -- the rejection test that names the 500 MHz config it calls out.", "Static timing as arithmetic: 25 tests pass natively on arcs, slack and Fmax, with the Artix-7 teaching delays in the header.", "The three reset kinds through the real compiler: async assert with sync release through 2 to 4 flops, and the racy async release, 16 tests natively.", "What the compiler emits for the reset pipe: t27c gen prints the Zig, one struct copy per clock, and wc -l counts it.", "Metastability MTBF without floats: the WP323 form in Q10 log2, one flop negative slack, two flops astronomical, 15 tests natively.", "The CDC model as Zig: t27c gen prints what it compiled and wc -l says how many lines of code the model is.", "The FIFO stdlib through the real compiler: 18 tests pass natively and 6 invariants are proved comptime.", "Move a pointer by 3 instead of 1 and every test still passes: the suite asserts fill and flags, never pointer values.", "Flip one flag assignment and a single test objects: the guard that keeps the FIFO honest.", "The pins-to-XDC emitter spec parsed by the real compiler, and the create_clock lines it exists to write.", "The fpga-rxcap tool card as a spec: constants for a tool page, and a native run that says 0 tests and why.", "168 records: 14 papers entered twice, 141 cite no score, 144 state zero at pass@1.", "67 specs lose 23,578 tokens to parser recovery, each held at its pinned bound.", "Smallest hold 3445 ps on TXD1 against a 1000 ps bench; each TX pin setup and hold from the routed SDF.", "Per-bit setup and hold of an async RX nibble against its RXC edge, from the routed SDF: one nibble changes 2387 to 2486 ps apart, so a binary counter tears where a gray one cannot.", "RX_ER pulses injected at exact nibble positions of a ping -- preamble, mid-frame, IP header -- and the frame state each pulse leaves behind, RTL vs model.", "The Ethernet bring-up plan on the bench: pre-registered steps, logged exits, what ran and what did not.", "The registered IO clients on the bench, read live: names, claims and what each declares it owns.", "The bench claim in action: one client takes the hardware lock and the tool reports who holds it.", "The bench lock given back: the client gives up its claim and the bench reports itself free.", "The next queued step on the bench, read live from the run state.", "The memory map spec through the real compiler: 15 tests pass natively and 6 invariants are proved comptime.", "The SPI testbench through the real compiler: 7 tests pass natively, driving the modes and checking the captures.", "t27c check on the UART spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the SPI spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the APB bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the AXI4 spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the bus bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the memory spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "The UART spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The SPI spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The APB bridge spec lowered to synthesizable Verilog: port list, PSEL and PENABLE wiring, the address decode.", "The AXI4 spec lowered to synthesizable Verilog: the five channels' wiring and handshakes as generated wires.", "The bus bridge spec lowered to synthesizable Verilog: both sides of the crossing as generated wires.", "The whole design in one spec lowered to synthesizable Verilog: clocks, resets and bus wiring as the compiler emits them.", "The UART testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The APB bridge testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The AXI4 testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The memory testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The integration testbench spec lowered to Verilog: every bus of the design in one bench.", "The top-level testbench spec lowered to Verilog: the whole design under one bench.", "The SPI testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The UART spec lowered to hardware IR: the compiler's own view of the wires and state before Verilog exists.", "The APB bridge spec lowered to hardware IR: PSEL, PENABLE and the decode as the compiler structures them.", "The AXI4 spec lowered to hardware IR: the five channels and their handshakes as the compiler structures them.", "The Ethernet FCS spec through the real compiler: 8 tests pass natively -- the table derived from one bit step, the 0xCBF43926 check value, the 0xDEBB20E3 receiver residue.", "The RGMII spec through the real compiler: 7 tests pass natively and 1 invariant is proved comptime -- the 8 ns gigabit period, a nibble on both edges, the 78 ps tap ladder.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "", "", "The OCP MX scale byte as a spec: 2^(e-127), code 255 is NaN, 18 tests and 9 comptime invariants, compiled on a real machine.", "The same scale byte compiled in your browser: seven backends, the spec's tests run in the page, and every skip says why.", "One outlier sets the scale for 32 weights: MXFP4 flushes 23 of them to zero, MXFP8 E4M3 none. 49 tests pass; a planted bug fails one.", "Ternary weights scaled by phi, compiled in your browser: codes 00, 01, 10, and the fourth code folded to zero.", "13 of 13 tests pass natively, and the report says 8 of them executed no assert; a planted bug fails exactly one.", "Five measured rules for a ternary weight alphabet, and the one formula that passes all five, run in your browser.", "One more trit and the spec does not compile at all: BLOCKED, not failed, because no test ever ran.", "A 17-bit float whose exponent is four balanced trits, 3^4 = 81 values, compiled in your browser.", "34 tests pass natively, none vacuous; swap one XOR for OR and only the test that negates twice fails.", "A neuron's sum squeezed back to one trit: 7 tests pass natively, and turning one > into >= fails only the boundary test.", "One ternary neuron, two functions picked by its weights: 12 tests pass natively, and one weight moved from P to Z fails exactly one.", "A neuron wider than 27 inputs, summed chunk by chunk: 11 tests pass natively, and a loop that stops after one chunk fails exactly one.", "A 27-lane ternary dot product as pure wires: 4 tests pass natively, and one wrong sign in tmul fails exactly one.", "The same dot product added into a register on every clock: 4 tests pass natively, and a lane mask short one bit fails exactly one.", "A whole small ternary network, 3 neurons then 2: 4 tests pass natively, and a trit packed one bit low fails exactly one.", "A signed multiply: 3 tests pass natively, none vacuous, and a sign stuck at 0 fails exactly one, m2.", "A signed add: 3 tests pass natively, and adding where opposite signs must subtract fails exactly one, a2.", "Two signed products summed: 4 tests pass natively, and OR in place of the sign XOR fails exactly one, pp.", "A ReLU: 4 tests pass natively, and letting negative inputs through fails only negz, the one test with a negative input.", "2^x for a softmax: 4 tests pass natively, and dropping the minus sign of k fails exactly one, em1.", "argmax by comparing bits alone: 4 tests pass natively, and >= in place of > fails exactly one, tie_low.", "A loss in bits: 3 tests pass natively, and a zero with the sign bit set fails exactly one, perfect.", "One SGD step: 3 tests pass natively, and g times g in place of eta times g fails exactly one, ascend.", "A 2-layer ReLU net for XOR: 4 tests pass natively, and one relu removed fails exactly one, x00.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "34 vector files, 512 cases: 2 files executed, 8 debt, 24 prose-only; 19% carry data.", "Four census populations recounted another way all agree; three are named as uncheckable.", "31 of 1,428 seals record no output (16 specs); 3 of those are missing from the debt ledger.", "11 specs refused at parse, 0 blocked only by prose, 23 broken on purpose as fixtures.", "12 Lean files, 15,553 lines and 4 sorry, are reached by no build root: nothing compiles them.", "tri, railway, python3 and the loop directory: each present or not, in one call.", "The terminal for seven t27 backends: every command run on hello_world. 21 pass, 0 fail.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters.", "", "", "73 published posts with date, language and revision, newest first.", "6 of 6 self-tests hold: anomalies, blog_drift, lessons, site_gate, tick, vault."]; +pub const WIDGET_HOOKS : [306]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "17 of 17 conformance specs compile, their test blocks pass, and no generated file has drifted.", "14 of 14 bench tools pass their own self-tests in one sweep: decode 18/18, repin 17/17, firelog 10/10, tmpcheck 9/9, stamps 9/9, keep 9/9.", "Requested 115,200 puts 115,385 on the wire (+0.16 %), 921,600 puts 923,077, and 3,000,000 is exact.", "26 checks, no board, no RTL: every receipt returns, 11 loss and fault cases recover, and a lie stops the run.", "One command reads the bench: cables, locks, and the last board runs: three x7-board runs PASS 51840/51840.", "112 files under /tmp pinned by sha256 in specs: every one has a kept copy, 0 lost, 0 unlocated.", "84 board and model logs scanned for the TRI-NET key: 0 hits, 0 logs of 1 MB or more.", "16 spec-named scripts run their own --self-test, 0 bad; 8 without the flag are listed, not skipped silently.", "The part table in far.t27 matches prjxray-db part.json, all 6 reference bitstreams present, bitwalk reads all 6 in 84.4 ms.", "One command re-runs every sweep: CRC 12/12, ECC 32520/32520 frames, 82/82 pins, 4224/4224 segbits, gold 5/5. audit: CLEAN.", "The CDC spec through the real compiler: 12 tests pass natively and the header says what the model does and does not claim.", "The tri clocks card as a spec: constants a tool page reads, and a native test-report that says so plainly -- 0 tests, because it is not a test suite.", "The board itself as a spec: 15 tests pass natively and the part line names the xc7a200tfgg676 the Wukong carries.", "Clock-tree synthesis in numbers: 15 tests pass natively, and the header pins the Artix-7 teaching delays the tree arithmetic uses.", "The MMCM model through the real compiler: VCO window, M/D/O ranges, fractional multiply in eighths, the 1/56 phase step, 16 tests natively.", "Widen the VCO window with one sed and exactly one test objects -- the rejection test that names the 500 MHz config it calls out.", "Static timing as arithmetic: 25 tests pass natively on arcs, slack and Fmax, with the Artix-7 teaching delays in the header.", "The three reset kinds through the real compiler: async assert with sync release through 2 to 4 flops, and the racy async release, 16 tests natively.", "What the compiler emits for the reset pipe: t27c gen prints the Zig, one struct copy per clock, and wc -l counts it.", "Metastability MTBF without floats: the WP323 form in Q10 log2, one flop negative slack, two flops astronomical, 15 tests natively.", "The CDC model as Zig: t27c gen prints what it compiled and wc -l says how many lines of code the model is.", "The FIFO stdlib through the real compiler: 18 tests pass natively and 6 invariants are proved comptime.", "Move a pointer by 3 instead of 1 and every test still passes: the suite asserts fill and flags, never pointer values.", "Flip one flag assignment and a single test objects: the guard that keeps the FIFO honest.", "The pins-to-XDC emitter spec parsed by the real compiler, and the create_clock lines it exists to write.", "The fpga-rxcap tool card as a spec: constants for a tool page, and a native run that says 0 tests and why.", "168 records: 14 papers entered twice, 141 cite no score, 144 state zero at pass@1.", "67 specs lose 23,578 tokens to parser recovery, each held at its pinned bound.", "Smallest hold 3445 ps on TXD1 against a 1000 ps bench; each TX pin setup and hold from the routed SDF.", "Per-bit setup and hold of an async RX nibble against its RXC edge, from the routed SDF: one nibble changes 2387 to 2486 ps apart, so a binary counter tears where a gray one cannot.", "RX_ER pulses injected at exact nibble positions of a ping -- preamble, mid-frame, IP header -- and the frame state each pulse leaves behind, RTL vs model.", "The Ethernet bring-up plan on the bench: pre-registered steps, logged exits, what ran and what did not.", "The registered IO clients on the bench, read live: names, claims and what each declares it owns.", "The bench claim in action: one client takes the hardware lock and the tool reports who holds it.", "The bench lock given back: the client gives up its claim and the bench reports itself free.", "The next queued step on the bench, read live from the run state.", "The memory map spec through the real compiler: 15 tests pass natively and 6 invariants are proved comptime.", "The SPI testbench through the real compiler: 7 tests pass natively, driving the modes and checking the captures.", "t27c check on the UART spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the SPI spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the APB bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the AXI4 spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the bus bridge spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "t27c check on the memory spec: 0 errors, 0 warnings -- the first bar every lesson's spec clears.", "The UART spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The SPI spec lowered to synthesizable Verilog: the TRINITY banner, the module port list and the datapath as wires.", "The APB bridge spec lowered to synthesizable Verilog: port list, PSEL and PENABLE wiring, the address decode.", "The AXI4 spec lowered to synthesizable Verilog: the five channels' wiring and handshakes as generated wires.", "The bus bridge spec lowered to synthesizable Verilog: both sides of the crossing as generated wires.", "The whole design in one spec lowered to synthesizable Verilog: clocks, resets and bus wiring as the compiler emits them.", "The UART testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The APB bridge testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The AXI4 testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The memory testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The integration testbench spec lowered to Verilog: every bus of the design in one bench.", "The top-level testbench spec lowered to Verilog: the whole design under one bench.", "The SPI testbench spec lowered to Verilog: what a bench looks like when the compiler emits it.", "The UART spec lowered to hardware IR: the compiler's own view of the wires and state before Verilog exists.", "The APB bridge spec lowered to hardware IR: PSEL, PENABLE and the decode as the compiler structures them.", "The AXI4 spec lowered to hardware IR: the five channels and their handshakes as the compiler structures them.", "The Ethernet FCS spec through the real compiler: 8 tests pass natively -- the table derived from one bit step, the 0xCBF43926 check value, the 0xDEBB20E3 receiver residue.", "The RGMII spec through the real compiler: 7 tests pass natively and 1 invariant is proved comptime -- the 8 ns gigabit period, a nibble on both edges, the 78 ps tap ladder.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "", "", "The OCP MX scale byte as a spec: 2^(e-127), code 255 is NaN, 18 tests and 9 comptime invariants, compiled on a real machine.", "The same scale byte compiled in your browser: seven backends, the spec's tests run in the page, and every skip says why.", "One outlier sets the scale for 32 weights: MXFP4 flushes 23 of them to zero, MXFP8 E4M3 none. 49 tests pass; a planted bug fails one.", "Ternary weights scaled by phi, compiled in your browser: codes 00, 01, 10, and the fourth code folded to zero.", "13 of 13 tests pass natively, and the report says 8 of them executed no assert; a planted bug fails exactly one.", "Five measured rules for a ternary weight alphabet, and the one formula that passes all five, run in your browser.", "One more trit and the spec does not compile at all: BLOCKED, not failed, because no test ever ran.", "A 17-bit float whose exponent is four balanced trits, 3^4 = 81 values, compiled in your browser.", "34 tests pass natively, none vacuous; swap one XOR for OR and only the test that negates twice fails.", "A neuron's sum squeezed back to one trit: 7 tests pass natively, and turning one > into >= fails only the boundary test.", "One ternary neuron, two functions picked by its weights: 12 tests pass natively, and one weight moved from P to Z fails exactly one.", "A neuron wider than 27 inputs, summed chunk by chunk: 11 tests pass natively, and a loop that stops after one chunk fails exactly one.", "A 27-lane ternary dot product as pure wires: 4 tests pass natively, and one wrong sign in tmul fails exactly one.", "The same dot product added into a register on every clock: 4 tests pass natively, and a lane mask short one bit fails exactly one.", "A whole small ternary network, 3 neurons then 2: 4 tests pass natively, and a trit packed one bit low fails exactly one.", "A signed multiply: 3 tests pass natively, none vacuous, and a sign stuck at 0 fails exactly one, m2.", "A signed add: 3 tests pass natively, and adding where opposite signs must subtract fails exactly one, a2.", "Two signed products summed: 4 tests pass natively, and OR in place of the sign XOR fails exactly one, pp.", "A ReLU: 4 tests pass natively, and letting negative inputs through fails only negz, the one test with a negative input.", "2^x for a softmax: 4 tests pass natively, and dropping the minus sign of k fails exactly one, em1.", "argmax by comparing bits alone: 4 tests pass natively, and >= in place of > fails exactly one, tie_low.", "A loss in bits: 3 tests pass natively, and a zero with the sign bit set fails exactly one, perfect.", "One SGD step: 3 tests pass natively, and g times g in place of eta times g fails exactly one, ascend.", "A 2-layer ReLU net for XOR: 4 tests pass natively, and one relu removed fails exactly one, x00.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "34 vector files, 512 cases: 2 files executed, 8 debt, 24 prose-only; 19% carry data.", "Four census populations recounted another way all agree; three are named as uncheckable.", "31 of 1,428 seals record no output (16 specs); 3 of those are missing from the debt ledger.", "11 specs refused at parse, 0 blocked only by prose, 23 broken on purpose as fixtures.", "12 Lean files, 15,553 lines and 4 sorry, are reached by no build root: nothing compiles them.", "tri, railway, python3 and the loop directory: each present or not, in one call.", "The terminal for seven t27 backends: every command run on hello_world. 21 pass, 0 fail.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters.", "", "", "73 published posts with date, language and revision, newest first.", "6 of 6 self-tests hold: anomalies, blog_drift, lessons, site_gate, tick, vault."]; ; --- Ideas: asked for, not built --------------------------------------------------------------- ; From what engineers share today (gh-skyline, Tiny Tapeout renders, Wrapped cards, Gatle-style @@ -119,9 +119,9 @@ test the_fpga_shelf_comes_first_and_opens_on_the_board { } test the_arena_is_a_tab_and_says_its_own_words { - assert WIDGET_IDS[271] == "wars"; - assert WIDGET_KINDS[271] == "tab"; - assert WIDGET_PAGES[271] == "wars"; + assert WIDGET_IDS[298] == "wars"; + assert WIDGET_KINDS[298] == "tab"; + assert WIDGET_PAGES[298] == "wars"; } test the_gallery_posts_nothing { @@ -157,8 +157,8 @@ test the_verification_shelf_is_rehearsals_and_audits { test ideas_are_counted_apart_from_widgets { assert IDEA_COUNT == 6; - assert WIDGET_IDS[276] == "gatle"; - assert WIDGET_COUNT == 279; + assert WIDGET_IDS[303] == "gatle"; + assert WIDGET_COUNT == 306; } test the_ternary_machine_shelf_runs_before_the_arena { @@ -172,9 +172,9 @@ test every_tool_lives_under_widgets_and_keeps_its_words_on_its_page { assert WIDGET_KINDS[9] == "tool"; assert WIDGET_PAGES[9] == "widgets/bit-diff/"; assert WIDGET_HOOKS[9] == ""; - assert WIDGET_KINDS[275] == "tool"; - assert WIDGET_CATEGORIES[275] == "game"; - assert WIDGET_PAGES[275] == "widgets/race-the-bee/"; + assert WIDGET_KINDS[302] == "tool"; + assert WIDGET_CATEGORIES[302] == "game"; + assert WIDGET_PAGES[302] == "widgets/race-the-bee/"; } test the_arithmetic_shelf_closes_the_fpga_bench { @@ -192,5 +192,14 @@ test the_neural_network_shelf_runs_before_the_arena { assert WIDGET_PAGES[244] == "widgets/nn-relu/"; assert WIDGET_IDS[270] == "nn-ppo"; assert WIDGET_CATEGORIES[270] == "compiler"; - assert WIDGET_IDS[271] == "wars"; + assert WIDGET_IDS[298] == "wars"; +} + +test the_hive_shelf_runs_before_the_arena { + assert WIDGET_IDS[271] == "hive-shape"; + assert WIDGET_KINDS[271] == "tool"; + assert WIDGET_PAGES[271] == "widgets/hive-shape/"; + assert WIDGET_IDS[297] == "hive-cell"; + assert WIDGET_CATEGORIES[297] == "game"; + assert WIDGET_IDS[298] == "wars"; } diff --git a/apps/website/specs/widgets/hive-aging.t27 b/apps/website/specs/widgets/hive-aging.t27 new file mode 100644 index 0000000000..f2eea305d7 --- /dev/null +++ b/apps/website/specs/widgets/hive-aging.t27 @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-aging.t27 -- Aging: how long a labelled low issue waits +; Source of truth for public/widgets/hive-aging/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-aging/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/priority.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 4 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a level, whether it is labelled, and an age from 0 to 60 days. The +; green line is effective_level over every day for a labelled issue and the dashed one for an +; unlabelled one; the tiles read it and why at the chosen age. + +; WHAT IT DOES NOT CLAIM: it does not read GitHub labels: the level is what the reader picks. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_aging; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-aging"; +pub const TITLE : str = "Aging: how long a labelled low issue waits"; +pub const DESCRIPTION : str = "Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical."; +pub const IMAGE_ALT : str = "Aging card: a labelled LOW issue at 20 days. The green step line runs at low until day 14 and then at normal to day 60; the dashed unlabelled line stays flat. Tiles read effective level normal, why aged, 14 days a step."; +pub const HOOK : str = "A low label that aged past unlabelled work would make low the fastest way to the front. So aging is one step, and only for labelled issues."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is effective_level and why, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 4 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_AGE_MAX : u8 = 60; +pub const K_AGE_TICKS : [5]u8 = [0, 14, 28, 42, 60]; +pub const K_AGING_DAYS : u32 = 14; +pub const K_MAX_AGING_STEPS : u32 = 1; +pub const K_PRESET_LEVEL : [4]u8 = [3, 2, 3, 1]; +pub const K_PRESET_LABELLED : [4]u8 = [1, 1, 0, 1]; +pub const K_PRESET_AGE : [4]u8 = [20, 30, 50, 45]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Low, 20 days", "Normal, 30 days", "Low, no label", "High, 45 days"]; +pub const SAY_ASK : str = "How long does a labelled low issue wait before it moves up?"; +pub const SAY_LEVEL : str = "level"; +pub const SAY_LEVEL_NAMES : [4]str = ["critical", "high", "normal", "low"]; +pub const SAY_LABELLED : str = "labelled"; +pub const SAY_AGE : str = "days open"; +pub const SAY_PLOT_LABEL : str = "the level an issue runs at, by days open"; +pub const SAY_LINE : str = "labelled"; +pub const SAY_FLAT : str = "not labelled"; +pub const SAY_PICKED : str = "level picked"; +pub const SAY_EFFECTIVE : str = "runs at"; +pub const SAY_STEP : str = "days a step"; +pub const SAY_WHY : str = "why"; +pub const SAY_WHY_NAMES : [5]str = ["listing", "label", "aged", "capped", "blocked"]; +pub const SAY_NOTE : str = "Lower runs first. An unlabelled issue is normal and never ages, so a repository with no labels keeps the order it had. High and critical do not age at all."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/priority.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test one_step_every_fourteen_days { + assert K_AGING_DAYS == 14; + assert K_MAX_AGING_STEPS == 1; + assert K_AGE_TICKS[1] == K_AGING_DAYS; +} + +test the_presets_fit_the_axis { + assert K_PRESET_AGE[2] <= K_AGE_MAX; + assert K_PRESET_LEVEL[0] == 3; + assert K_AGE_TICKS[4] == K_AGE_MAX; +} diff --git a/apps/website/specs/widgets/hive-boundary.t27 b/apps/website/specs/widgets/hive-boundary.t27 new file mode 100644 index 0000000000..06b989b0a1 --- /dev/null +++ b/apps/website/specs/widgets/hive-boundary.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-boundary.t27 -- A boundary: which files a task may reserve +; Source of truth for public/widgets/hive-boundary/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-boundary/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/shaping.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 2 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: each of four paths carries four facts the reader taps: the file exists, its folder +; exists, the issue cites it, it would be a new file in a language the only-t27 rule denies. Each +; path is path_accepted of its facts; the gate is shape_lands(missing after, 4, refused). + +; WHAT IT DOES NOT CLAIM: it does not read a repository: the four paths and their facts are what +; the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_boundary; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-boundary"; +pub const TITLE : str = "A boundary: which files a task may reserve"; +pub const DESCRIPTION : str = "Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds."; +pub const IMAGE_ALT : str = "Boundary card: four proposed paths. An existing spec and a new cited t27 file in an existing folder are green; a cited file in a folder that does not exist and a new cited .py file are red. The gate is shut: 2 of 4 paths refused, the shape does not land."; +pub const HOOK : str = "A wrong boundary is worse than a missing one: it reserves files the work does not own. An invented path is refused, and one refusal stops the shape."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is path_accepted and shape_lands, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 2 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_PATHS : u8 = 4; +pub const K_MAX_BOUNDARY_PATHS : u32 = 12; +pub const K_FACT_BITS : [4]u8 = [1, 2, 4, 8]; +pub const K_PRESET_P0 : [4]u8 = [1, 1, 1, 1]; +pub const K_PRESET_P1 : [4]u8 = [6, 6, 6, 0]; +pub const K_PRESET_P2 : [4]u8 = [4, 6, 4, 4]; +pub const K_PRESET_P3 : [4]u8 = [14, 6, 14, 14]; +pub const K_PRESET_LEFT : [4]u8 = [0, 0, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Two invented paths", "Every path checked", "Still a section missing", "Only real files"]; +pub const SAY_ASK : str = "Which files may a task reserve before a bee starts on it?"; +pub const SAY_PATHS : [4]str = ["specs/queen/hive.t27", "specs/queen/comb.t27", "tools/queen/draw/new.t27", "tools/queen/draw.py"]; +pub const SAY_FACTS : [4]str = ["exists", "folder exists", "cited by the issue", "new, foreign language"]; +pub const SAY_LEFT : str = "a section still missing after the shape"; +pub const SAY_OK : str = "accepted"; +pub const SAY_BAD : str = "refused"; +pub const SAY_LANDS : str = "the shape lands"; +pub const SAY_STOPS : str = "the shape does not land"; +pub const SAY_COUNT : str = "{0} of {1} paths refused"; +pub const SAY_NOTE : str = "A file that exists is always fine: finding the file a title describes is the point. A new file needs a folder that exists, a citation in the issue, and a language the only-t27 rule allows. More than 12 paths is not a task but a project."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/shaping.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_paths_four_facts { + assert K_PATHS == 4; + assert K_FACT_BITS[0] == 1; + assert K_FACT_BITS[3] == 8; +} + +test the_start_has_one_path_of_each_kind { + assert K_PRESET_P0[0] == 1; + assert K_PRESET_P1[0] == 6; + assert K_PRESET_P3[0] == 14; + assert K_PATHS <= K_MAX_BOUNDARY_PATHS; +} diff --git a/apps/website/specs/widgets/hive-burn.t27 b/apps/website/specs/widgets/hive-burn.t27 new file mode 100644 index 0000000000..6fbb21cc7e --- /dev/null +++ b/apps/website/specs/widgets/hive-burn.t27 @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-burn.t27 -- Tokens that burn: a month to spend them +; Source of truth for public/widgets/hive-burn/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-burn/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/token-wallet.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 21 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags today from day 0 to day 60. Each batch is days_left(the day it +; landed, today, 30). The list is ordered by those answers, which is the spend order the spec names, +; soonest burn first. + +; WHAT IT DOES NOT CLAIM: the spec marks the burn as decided but not yet in the ledger; the page +; shows the rule, not a balance. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_burn; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-burn"; +pub const TITLE : str = "Tokens that burn: a month to spend them"; +pub const DESCRIPTION : str = "Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first."; +pub const IMAGE_ALT : str = "Burn card on day 25: three bars on a 60-day strip. The package month from day 0 has 5 days left, gold; the start gift from day 3 has 8 days; the top-up from day 12 has 17 days, green. A white line marks today."; +pub const HOOK : str = "All money goes into tokens, and the tokens burn within the month: a person who paid makes content instead of saving tokens."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is days_left, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 21 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_TODAY_MAX : u8 = 60; +pub const K_MONTH_DAYS : u16 = 30; +pub const K_BATCH_DAYS : [3]u8 = [0, 3, 12]; +pub const K_PRESET_TODAY : [4]u8 = [25, 2, 31, 45]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Day 25", "Day 2", "Day 31, the month burned", "Day 45"]; +pub const SAY_ASK : str = "How long do tokens last, and which ones are spent first?"; +pub const SAY_TODAY : str = "today, day"; +pub const SAY_BATCHES : [3]str = ["the package month", "the start gift", "a top-up"]; +pub const SAY_LANDED : str = "landed on day {0}"; +pub const SAY_LEFT : str = "{0} days left"; +pub const SAY_GONE : str = "burned"; +pub const SAY_STRIP : str = "days 0 to 60, one bar a batch"; +pub const SAY_ORDER : str = "spent first: the soonest to burn"; +pub const SAY_NOTE : str = "The spec warns at 7, 3 and 1 days. Tokens held before the burn existed never burn. Whether bought top-ups should live longer is still the owner's call: one line, TOPUP_DAYS."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/token-wallet.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test a_month_is_thirty_days { + assert K_MONTH_DAYS == 30; + assert K_TODAY_MAX == K_MONTH_DAYS * 2; +} + +test the_batches_land_in_the_first_month { + assert K_BATCH_DAYS[2] < K_MONTH_DAYS; + assert K_PRESET_TODAY[0] == 25; +} diff --git a/apps/website/specs/widgets/hive-called.t27 b/apps/website/specs/widgets/hive-called.t27 new file mode 100644 index 0000000000..65f982237e --- /dev/null +++ b/apps/website/specs/widgets/hive-called.t27 @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-called.t27 -- Who is called: an agent bot in a group of agents +; Source of truth for public/widgets/hive-called/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-called/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 22 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader toggles tagged, the all tag, a reply to this agent, and the agent's own +; topic. The two cards are addressed(flags, from a person) and addressed(flags, from a bot); every +; table cell is addressed of its row's single flag and its column's sender. + +; WHAT IT DOES NOT CLAIM: it sends nothing to Telegram; a live group also needs Bot-to-Bot +; Communication Mode on in @BotFather. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_called; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-called"; +pub const TITLE : str = "Who is called: an agent bot in a group of agents"; +pub const DESCRIPTION : str = "Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot."; +pub const IMAGE_ALT : str = "Called card: a message written in the agent's own topic, with no tag and no reply. The person's message wakes the agent, green; the same message from another agent's bot does not, grey. A table of five ways to call reads tag, reply, all tag, own topic, nothing for both senders."; +pub const HOOK : str = "A bot never triggers the whole hive and never wakes a topic's owner by merely writing there. Only a tag or a reply calls an agent from another bot."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is addressed, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 22 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_FLAGS : u8 = 4; +pub const K_PRESET_TAGGED : [4]u8 = [0, 1, 0, 0]; +pub const K_PRESET_ALL : [4]u8 = [0, 0, 1, 0]; +pub const K_PRESET_REPLY : [4]u8 = [0, 0, 0, 1]; +pub const K_PRESET_TOPIC : [4]u8 = [1, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["In the agent's topic", "Tagged by name", "The all tag", "A reply to the agent"]; +pub const SAY_ASK : str = "A message lands in the hive group. Which agent must answer it?"; +pub const SAY_FLAGS : [4]str = ["tagged by name", "the @all tag", "a reply to this agent", "in this agent's topic"]; +pub const SAY_SENDERS : [2]str = ["a person writes", "another agent's bot writes"]; +pub const SAY_CALLED : str = "called: answers"; +pub const SAY_QUIET : str = "not called: stays quiet"; +pub const SAY_TABLE : str = "one way at a time"; +pub const SAY_NOTHING : str = "none of them"; +pub const SAY_YES : str = "calls"; +pub const SAY_NO : str = "quiet"; +pub const SAY_NOTE : str = "Telegram gives no loop protection between bots. So the rule is narrow for bots: a tag or a reply, nothing else, and the depth and the cooldown of the next two lessons stop the rest."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_ways_to_call { + assert K_FLAGS == 4; + assert K_PRESET_TOPIC[0] == 1; +} + +test each_preset_sets_one_flag { + assert K_PRESET_TAGGED[1] + K_PRESET_ALL[1] + K_PRESET_REPLY[1] + K_PRESET_TOPIC[1] == 1; + assert K_PRESET_TAGGED[0] + K_PRESET_ALL[0] + K_PRESET_REPLY[0] + K_PRESET_TOPIC[0] == 1; +} diff --git a/apps/website/specs/widgets/hive-cell.t27 b/apps/website/specs/widgets/hive-cell.t27 new file mode 100644 index 0000000000..2ef440ee68 --- /dev/null +++ b/apps/website/specs/widgets/hive-cell.t27 @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-cell.t27 -- The comb answers: what an event does to a cell +; Source of truth for public/widgets/hive-cell/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-cell/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/hive.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 27 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks an event kind, whether a review accepted, the milliseconds since +; the event, and for new cells the count pending, whether cells were placed before and the seconds +; since the last batch. Each cell is cell_effect(kind, accepted) at flash_alpha(ms); the gate is +; cells_due. + +; WHAT IT DOES NOT CLAIM: it is a flat comb, not the 3D game, and reads no live events. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_cell; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-cell"; +pub const TITLE : str = "The comb answers: what an event does to a cell"; +pub const DESCRIPTION : str = "Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells."; +pub const IMAGE_ALT : str = "Comb card: ten cells, one per event kind, 400 ms after a review that accepted. The review cell flashes gold at 778 of 1000; task created is white, lease expired red, cancel grey, the six others dark. A gate below reads new cells due: no, 120 s of 300 since the last batch."; +pub const HOOK : str = "Before this spec an accepted task, a lost lease and a cancel looked the same as no news. Now each paints its cell, and the flash fades over 1800 ms."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is cell_effect, flash_alpha and cells_due, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 27 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KINDS : u8 = 10; +pub const K_MS_MAX : u16 = 2000; +pub const K_MS_STEP : u8 = 50; +pub const K_FLASH_MS : u32 = 1800; +pub const K_CELL_BATCH_SECONDS : u32 = 300; +pub const K_PENDING_MAX : u8 = 10; +pub const K_SINCE_MAX : u16 = 600; +pub const K_SINCE_STEP : u8 = 10; +pub const K_FX_TONES : [6]u8 = [9, 7, 1, 4, 6, 8]; +pub const K_PRESET_KIND : [4]u8 = [5, 5, 3, 0]; +pub const K_PRESET_ACCEPTED : [4]u8 = [1, 0, 0, 0]; +pub const K_PRESET_MS : [4]u16 = [400, 200, 900, 0]; +pub const K_PRESET_PENDING : [4]u8 = [3, 3, 1, 5]; +pub const K_PRESET_PLACED : [4]u8 = [1, 1, 1, 0]; +pub const K_PRESET_SINCE : [4]u16 = [120, 320, 30, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A review accepted", "A review sent back", "A lease expired", "The page opens"]; +pub const SAY_ASK : str = "How does the game show that something happened to a task?"; +pub const SAY_KIND : str = "event"; +pub const SAY_KIND_SHORT : [10]str = ["created", "ended", "idle", "lease", "evidence", "review", "assign", "cancel", "beat", "tick"]; +pub const SAY_KIND_NAMES : [10]str = ["task created", "task ended", "worker idle", "lease expired", "evidence", "review", "assign", "cancel", "heartbeat", "tick"]; +pub const SAY_ACCEPTED : str = "the review accepted"; +pub const SAY_MS : str = "ms since the event"; +pub const SAY_PENDING : str = "new cells waiting"; +pub const SAY_PLACED : str = "cells placed before"; +pub const SAY_SINCE : str = "seconds since the last batch"; +pub const SAY_FX_NAMES : [6]str = ["nothing", "START, white", "ACCEPT, gold", "SEND BACK, amber", "LOST, red", "CANCEL, grey"]; +pub const SAY_FLASH : str = "flash, of 1000"; +pub const SAY_DUE : str = "new cells are placed now"; +pub const SAY_NOT_DUE : str = "new cells wait for the next batch"; +pub const SAY_BATCH : str = "{0} waiting, {1} s of {2}"; +pub const SAY_NOTE : str = "A new cell rebuilds the whole scene, so cells are placed in batches at most every 300 s, and all at once when the page first opens. A task ended paints nothing: its ship flies home, which says it."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/hive.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test ten_kinds_and_their_clocks { + assert K_KINDS == 10; + assert K_FLASH_MS == 1800; + assert K_CELL_BATCH_SECONDS == 300; +} + +test the_sliders_cross_each_clock { + assert K_MS_MAX > K_FLASH_MS; + assert K_SINCE_MAX > K_CELL_BATCH_SECONDS; + assert K_PRESET_KIND[0] == 5; +} diff --git a/apps/website/specs/widgets/hive-chain.t27 b/apps/website/specs/widgets/hive-chain.t27 new file mode 100644 index 0000000000..0fcd1288b6 --- /dev/null +++ b/apps/website/specs/widgets/hive-chain.t27 @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-chain.t27 -- Chain depth: human, A, B, C, then silence +; Source of truth for public/widgets/hive-chain/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-chain/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 23 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the milliseconds between one bot message and the next. The +; person's rung is may_answer(from a person); each agent rung is may_answer(from a bot, depth, ms) +; at its depth, and chain_open(depth) beside it. + +; WHAT IT DOES NOT CLAIM: it is a ladder of turns, not a live chat. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_chain; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-chain"; +pub const TITLE : str = "Chain depth: human, A, B, C, then silence"; +pub const DESCRIPTION : str = "One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it."; +pub const IMAGE_ALT : str = "Chain card: a person asks and agents answer 6 s apart. A ladder of six rungs: the person, agent A, agent B and agent C are lit green; agent D and agent E are grey, silent at depth 3. Tiles read depth limit 3 and cooldown 5000 ms."; +pub const HOOK : str = "Telegram has no loop protection between bots. Two numbers stop two agents answering each other forever: a depth of 3 and a cooldown of 5 s."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is chain_open and may_answer, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 23 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_GAP_MAX : u16 = 10000; +pub const K_GAP_STEP : u8 = 100; +pub const K_RUNGS : u8 = 6; +pub const K_MAX_BOT_DEPTH : u32 = 3; +pub const K_BOT_REPLY_COOLDOWN_MS : u32 = 5000; +pub const K_PRESET_GAP : [3]u16 = [6000, 3200, 5000]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["6 s apart", "3.2 s apart", "Exactly 5 s"]; +pub const SAY_ASK : str = "Two agents can answer each other. What stops them from doing it forever?"; +pub const SAY_GAP : str = "ms between one bot message and the next"; +pub const SAY_RUNG_NAMES : [6]str = ["a person asks", "agent A", "agent B", "agent C", "agent D", "agent E"]; +pub const SAY_DEPTH : str = "depth {0}"; +pub const SAY_OPEN : str = "chain open"; +pub const SAY_CLOSED : str = "chain closed"; +pub const SAY_ANSWERS : str = "answers"; +pub const SAY_SILENT : str = "silent"; +pub const SAY_LIMIT : str = "depth limit"; +pub const SAY_COOL : str = "cooldown, ms"; +pub const SAY_LAST : str = "last to speak"; +pub const SAY_NOTE : str = "Depth counts the bot messages already between this one and the person. A person is always answered, whatever the depth; a bot only inside the chain and after the cooldown."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test three_deep_five_seconds { + assert K_MAX_BOT_DEPTH == 3; + assert K_BOT_REPLY_COOLDOWN_MS == 5000; + assert K_RUNGS > K_MAX_BOT_DEPTH + 1; +} + +test the_start_is_past_the_cooldown { + assert K_PRESET_GAP[0] > K_BOT_REPLY_COOLDOWN_MS; + assert K_PRESET_GAP[2] == K_BOT_REPLY_COOLDOWN_MS; + assert K_PRESET_GAP[0] <= K_GAP_MAX; +} diff --git a/apps/website/specs/widgets/hive-column.t27 b/apps/website/specs/widgets/hive-column.t27 new file mode 100644 index 0000000000..45a8b97ca9 --- /dev/null +++ b/apps/website/specs/widgets/hive-column.t27 @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-column.t27 -- The public board: which column a task's card is in +; Source of truth for public/widgets/hive-column/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-column/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/dispatch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 26 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader says whether the issue is open and whether the issue list was read. Each +; of five claim cards sits in the column column_of(claim, issue open, list known) names. + +; WHAT IT DOES NOT CLAIM: it reads no board: the five claims are every claim the spec has. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_column; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-column"; +pub const TITLE : str = "The public board: which column a task's card is in"; +pub const DESCRIPTION : str = "Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict."; +pub const IMAGE_ALT : str = "Board card: the issue is closed and the issue list was read. Of six columns, running holds the running claim and done holds four cards, accepted, rejected, awaiting review and failed; backlog, blocked, review and dropped are empty."; +pub const HOOK : str = "The spec records it as a finding: the public board draws a closed issue's dispatch as done whatever its verdict, an escalation and a send-back included."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is column_of, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 26 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CLAIMS : u8 = 5; +pub const K_COLUMNS : u8 = 6; +pub const K_COL_DONE : u32 = 4; +pub const K_PRESET_OPEN : [3]u8 = [0, 1, 0]; +pub const K_PRESET_KNOWN : [3]u8 = [1, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["Issue closed", "Issue open", "List not read"]; +pub const SAY_ASK : str = "A task was reviewed and its issue was closed. Where does the public board draw it?"; +pub const SAY_OPEN : str = "the issue is open"; +pub const SAY_KNOWN : str = "the issue list was read"; +pub const SAY_COLUMN_NAMES : [6]str = ["backlog", "blocked", "running", "review", "done", "dropped"]; +pub const SAY_CLAIM_NAMES : [5]str = ["running", "accepted", "rejected", "awaiting review", "failed"]; +pub const SAY_CLAIM : str = "claim"; +pub const SAY_IN_DONE : str = "{0} of {1} cards in done"; +pub const SAY_NOTE : str = "Done should mean accepted or merged. Until the board reads verdicts for closed issues, a send-back on a closed issue looks finished on the board; the spec names it so a reader is not misled."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/dispatch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test five_claims_six_columns { + assert K_CLAIMS == 5; + assert K_COLUMNS == 6; +} + +test the_start_is_a_closed_issue { + assert K_PRESET_OPEN[0] == 0; + assert K_PRESET_KNOWN[0] == 1; +} diff --git a/apps/website/specs/widgets/hive-cooldown.t27 b/apps/website/specs/widgets/hive-cooldown.t27 new file mode 100644 index 0000000000..bef00a30c0 --- /dev/null +++ b/apps/website/specs/widgets/hive-cooldown.t27 @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-cooldown.t27 -- The cooldown clock, /stop and /mute +; Source of truth for public/widgets/hive-cooldown/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-cooldown/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/agent-hive-group.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 24 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the milliseconds since the last bot message, the minutes since +; /stop and since /mute, and whether the agent was tagged. The dial is cooled(ms); the cards are +; may_answer_after_stop(from a bot or a person, ms) and heard_while_muted(tagged, ms). + +; WHAT IT DOES NOT CLAIM: it sends no command: the minutes are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_cooldown; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-cooldown"; +pub const TITLE : str = "The cooldown clock, /stop and /mute"; +pub const DESCRIPTION : str = "One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard."; +pub const IMAGE_ALT : str = "Cooldown card: 3200 ms since the last bot message, the dial filling to 3.2 of 10 s with a gold mark at 5 s: not cooled, the bot waits. A /stop 4 minutes ago: bots stay silent, a person is heard. A /mute 20 minutes ago: only a tag by name gets through."; +pub const HOOK : str = "A stop cuts bot-to-bot turns only, a mute still answers its own tag: neither one can lose a person or a direct call."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/agent-hive-group.t27 (t27 master c42f8fb40): every value on the page is cooled, may_answer_after_stop and heard_while_muted, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 24 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MS_MAX : u16 = 10000; +pub const K_MS_STEP : u8 = 100; +pub const K_BOT_REPLY_COOLDOWN_MS : u32 = 5000; +pub const K_STOP_WINDOW_MS : u32 = 600000; +pub const K_MUTE_MS : u32 = 3600000; +pub const K_MS_PER_MINUTE : u32 = 60000; +pub const K_STOP_MAX : u8 = 20; +pub const K_MUTE_MAX : u8 = 90; +pub const K_PRESET_MS : [4]u16 = [3200, 5000, 8000, 1000]; +pub const K_PRESET_STOP : [4]u8 = [4, 12, 4, 20]; +pub const K_PRESET_MUTE : [4]u8 = [20, 20, 70, 5]; +pub const K_PRESET_TAGGED : [4]u8 = [0, 0, 0, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["3.2 s, stop 4 min ago", "Exactly 5 s", "Mute ran out", "Tagged while muted"]; +pub const SAY_ASK : str = "Agents answer fast. How does the hive keep them from flooding a chat?"; +pub const SAY_MS : str = "ms since the last bot message"; +pub const SAY_STOP : str = "minutes since /stop"; +pub const SAY_MUTE : str = "minutes since /mute"; +pub const SAY_TAGGED : str = "the agent is tagged by name"; +pub const SAY_COOLED : str = "cooled: a bot may answer"; +pub const SAY_WAIT : str = "not cooled: the bot waits"; +pub const SAY_DIAL : str = "of 10 s"; +pub const SAY_STOP_CARDS : [2]str = ["a bot after /stop", "a person after /stop"]; +pub const SAY_MUTE_CARD : str = "the muted agent"; +pub const SAY_HEARD : str = "heard"; +pub const SAY_SILENT : str = "silent"; +pub const SAY_NOTE : str = "/stop lasts 10 minutes and /mute an hour; both are clocks, so the hive needs no one to switch it back on."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/agent-hive-group.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_clocks_are_the_specs { + assert K_BOT_REPLY_COOLDOWN_MS == 5000; + assert K_STOP_WINDOW_MS == K_MS_PER_MINUTE * 10; + assert K_MUTE_MS == K_MS_PER_MINUTE * 60; +} + +test the_sliders_cross_each_window { + assert K_STOP_MAX * K_MS_PER_MINUTE > K_STOP_WINDOW_MS; + assert K_MUTE_MAX * K_MS_PER_MINUTE > K_MUTE_MS; + assert K_MS_MAX > K_BOT_REPLY_COOLDOWN_MS; +} diff --git a/apps/website/specs/widgets/hive-credit.t27 b/apps/website/specs/widgets/hive-credit.t27 new file mode 100644 index 0000000000..06047e7938 --- /dev/null +++ b/apps/website/specs/widgets/hive-credit.t27 @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-credit.t27 -- Lending a key earns no credit: XP follows recorded work +; Source of truth for public/widgets/hive-credit/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-credit/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/queen-contributor-keys.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 19 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the keys a contributor owns, the probes in flight and the seconds +; since the last probe. The credit meter is wallet_credit(keys added); the tiles are +; may_add_key(keys), may_start_probe(probes) and cooldown_passed(a probe ran, seconds). + +; WHAT IT DOES NOT CLAIM: it moves no money and stores no key: the wallet meter is the spec fn, and +; it answers 0. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_credit; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-credit"; +pub const TITLE : str = "Lending a key earns no credit: XP follows recorded work"; +pub const DESCRIPTION : str = "A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s."; +pub const IMAGE_ALT : str = "Credit card: a contributor with 99 keys. The keys meter is nearly full against the 100 limit and one more may be added; the wallet credit meter is empty at 0 tokens. A probe started 45 s ago waits for its 60 s cooldown; 3 of 4 probe slots are busy."; +pub const HOOK : str = "Adding a key never credits a wallet: CREDIT_WALLET is false in the spec, so XP follows only the work the Queen records, not the keys someone lends."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/queen-contributor-keys.t27 (t27 master c42f8fb40): every value on the page is wallet_credit, may_add_key, may_start_probe and cooldown_passed, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 19 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MAX_KEYS_PER_OWNER : u16 = 100; +pub const K_MAX_PROBES_IN_FLIGHT : u16 = 4; +pub const K_PROBE_COOLDOWN_SECONDS : u16 = 60; +pub const K_PROBES_MAX : u8 = 6; +pub const K_SECONDS_MAX : u8 = 120; +pub const K_PRESET_KEYS : [4]u8 = [99, 100, 3, 40]; +pub const K_PRESET_PROBES : [4]u8 = [3, 4, 0, 1]; +pub const K_PRESET_SECONDS : [4]u8 = [45, 61, 0, 90]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["99 keys, a probe 45 s ago", "At the limit", "A new contributor", "40 keys, probe cooled"]; +pub const SAY_ASK : str = "Does lending the hive a model key earn a contributor credit?"; +pub const SAY_KEYS : str = "keys this contributor owns"; +pub const SAY_PROBES : str = "probes in flight"; +pub const SAY_SECONDS : str = "seconds since the last probe (0: none yet)"; +pub const SAY_KEY_METER : str = "keys, limit {0}"; +pub const SAY_CREDIT : str = "wallet credit for those keys, tokens"; +pub const SAY_SLOT_METER : str = "probe slots in use"; +pub const SAY_ADD : str = "may add a key"; +pub const SAY_PROBE : str = "may start a probe"; +pub const SAY_COOL : str = "probe cooldown passed"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +; The verdict is filled with wallet_credit(keys): SAY_ZERO when it answers 0, SAY_CREDITED otherwise. +pub const SAY_ZERO : str = "{0} tokens: XP follows work"; +pub const SAY_CREDITED : str = "{0} tokens: the keys credit the wallet"; +pub const SAY_NOTE : str = "A key is probed before it is enabled, at most {0} at once and one per {1} s, so a burst of new keys cannot hammer a provider. Credit comes from the work the Queen records, never from the keys."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/queen-contributor-keys.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_limits_are_the_specs { + assert K_MAX_KEYS_PER_OWNER == 100; + assert K_MAX_PROBES_IN_FLIGHT == 4; + assert K_PROBE_COOLDOWN_SECONDS == 60; +} + +test the_presets_fit_the_sliders { + assert K_PRESET_KEYS[1] == K_MAX_KEYS_PER_OWNER; + assert K_PRESET_PROBES[1] <= K_PROBES_MAX; + assert K_PRESET_SECONDS[3] <= K_SECONDS_MAX; +} diff --git a/apps/website/specs/widgets/hive-drain.t27 b/apps/website/specs/widgets/hive-drain.t27 new file mode 100644 index 0000000000..74859a223f --- /dev/null +++ b/apps/website/specs/widgets/hive-drain.t27 @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-drain.t27 -- A deploy mid-flight: what happens to a running bee +; Source of truth for public/widgets/hive-drain/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-drain/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/drain.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 9 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the seconds waited, marks the work ended or not, and sets the +; platform drain. The cap is drain_cap_seconds(runner 1800, platform); each of the five kinds is +; drain_verdict(kind, ended, waited, cap). + +; WHAT IT DOES NOT CLAIM: the runner drain of 1800 s is the value the spec names for today; the +; page sends no signal. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_drain; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-drain"; +pub const TITLE : str = "A deploy mid-flight: what happens to a running bee"; +pub const DESCRIPTION : str = "The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store."; +pub const IMAGE_ALT : str = "Drain card at 1800 seconds of waiting, the cap. Five rows: bee HAND BACK in violet, review STOP in orange, round, job and wait LEAVE in sky blue. A violet meter shows 1800 of the 1800 s cap, the runner drain 1800 against the platform 1830."; +pub const HOOK : str = "A deploy does not cut a bee short: it is held up to the cap, then what it wrote is salvaged into its branch and its issue is free at once."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/drain.t27 (t27 master c42f8fb40): every value on the page is drain_verdict and drain_cap_seconds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 9 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_WAIT_MAX : u16 = 2400; +pub const K_WAIT_STEP : u8 = 60; +pub const K_RUNNER_DRAIN : u16 = 1800; +pub const K_PLATFORM_MAX : u16 = 3600; +pub const K_PLATFORM_STEP : u8 = 30; +pub const K_PRESET_WAIT : [4]u16 = [1800, 600, 60, 1800]; +pub const K_PRESET_ENDED : [4]u8 = [0, 0, 1, 0]; +pub const K_PRESET_PLATFORM : [4]u16 = [1830, 1830, 1830, 1000]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["At the cap", "Ten minutes in", "Work already ended", "A short platform drain"]; +pub const SAY_ASK : str = "What happens to a bee halfway through its task when the Queen is redeployed?"; +pub const SAY_WAIT : str = "seconds the drain has waited"; +pub const SAY_ENDED : str = "the work already ended"; +pub const SAY_PLATFORM : str = "platform drain, seconds"; +pub const SAY_KIND_NAMES : [5]str = ["a bee", "a review", "a round", "a job step", "a wait row"]; +pub const SAY_VERDICT_NAMES : [5]str = ["DONE", "LEAVE", "STOP", "HOLD", "HAND BACK"]; +pub const SAY_VERDICT_LINES : [5]str = ["nothing is owed", "the store keeps it; the next process takes it up", "not waited for; stopped when the process exits", "the drain waits for it to end", "stopped, salvaged into its branch, its issue free now"]; +pub const SAY_CAP : str = "cap, seconds"; +pub const SAY_WAITED : str = "waited"; +pub const SAY_NOTE : str = "A review is not held: a stop loses at most one review, which the next process redoes. A bee is held because what it has done lives in its conversation, which no row keeps."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/drain.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_reaches_past_the_cap { + assert K_WAIT_MAX > K_RUNNER_DRAIN; + assert K_PRESET_WAIT[0] == K_RUNNER_DRAIN; +} + +test the_platform_drain_is_in_range { + assert K_PRESET_PLATFORM[0] <= K_PLATFORM_MAX; + assert K_PRESET_PLATFORM[3] < K_RUNNER_DRAIN; +} diff --git a/apps/website/specs/widgets/hive-gate.t27 b/apps/website/specs/widgets/hive-gate.t27 new file mode 100644 index 0000000000..2f59be49b6 --- /dev/null +++ b/apps/website/specs/widgets/hive-gate.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-gate.t27 -- The merge gate: when an approved pull request may merge +; Source of truth for public/widgets/hive-gate/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-gate/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 12 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets how many checks posted, whether the ruleset is readable, and per +; check required, concluded, green and discounted. Each check is check_passes of its flags; the gate +; is gate_open(posted, running, blocking), the host counting the not concluded and the failing. + +; WHAT IT DOES NOT CLAIM: it does not read GitHub: the checks are what the reader taps. The merger +; itself is a workflow that meets this spec through a selftest. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_gate; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-gate"; +pub const TITLE : str = "The merge gate: when an approved pull request may merge"; +pub const DESCRIPTION : str = "Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks."; +pub const IMAGE_ALT : str = "Merge gate card: three checks posted. A required green check passes, an advisory red check that the bee discounted passes, and a required check still running blocks. The gate is shut, red: 1 running, 0 blocking."; +pub const HOOK : str = "Zero checks is not zero failures: a pull request with no posted check does not merge. And a red check counts as required whenever the ruleset cannot be read."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is check_passes and gate_open, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 12 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CHECKS : u8 = 4; +pub const K_FLAG_BITS : [4]u8 = [1, 2, 4, 8]; +pub const K_PRESET_POSTED : [4]u8 = [3, 2, 0, 2]; +pub const K_PRESET_RULESET : [4]u8 = [1, 1, 1, 0]; +pub const K_PRESET_C0 : [4]u8 = [7, 7, 0, 7]; +pub const K_PRESET_C1 : [4]u8 = [10, 10, 0, 10]; +pub const K_PRESET_C2 : [4]u8 = [1, 0, 0, 0]; +pub const K_PRESET_C3 : [4]u8 = [0, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["One check still running", "All concluded", "No check posted", "Ruleset unreadable"]; +pub const SAY_ASK : str = "When may the merger merge a pull request the reviewer bee approved?"; +pub const SAY_POSTED : str = "checks posted"; +pub const SAY_RULESET : str = "ruleset readable"; +pub const SAY_CHECK_NAMES : [4]str = ["build", "corpus ratchet", "website gate", "typecheck"]; +pub const SAY_FLAGS : [4]str = ["required", "concluded", "green", "discounted"]; +pub const SAY_PASSES : str = "passes"; +pub const SAY_BLOCKS : str = "blocks"; +pub const SAY_RUNNING : str = "running"; +pub const SAY_OPEN : str = "The gate opens: merge"; +pub const SAY_SHUT : str = "The gate stays shut"; +pub const SAY_COUNTS : str = "{0} posted, {1} running, {2} blocking"; +pub const SAY_NOTE : str = "A red check passes only when it is not required, has concluded and the bee named it as discounted. With the ruleset unreadable, every check counts as required, so no red one is discountable."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/merger_gate.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_checks_four_flags { + assert K_CHECKS == 4; + assert K_FLAG_BITS[3] == 8; +} + +test the_start_has_a_running_required_check { + assert K_PRESET_POSTED[0] == 3; + assert K_PRESET_C2[0] == 1; + assert K_PRESET_POSTED[2] == 0; +} diff --git a/apps/website/specs/widgets/hive-jam-action.t27 b/apps/website/specs/widgets/hive-jam-action.t27 new file mode 100644 index 0000000000..e33ab63464 --- /dev/null +++ b/apps/website/specs/widgets/hive-jam-action.t27 @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-jam-action.t27 -- The Queen's move: what each jam asks of her +; Source of truth for public/widgets/hive-jam-action/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-jam-action/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/review-jam.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 14 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the case from 0 to 255 or taps a preset. The verdict is +; jam_action(case); each of the ten case cards sits in the column jam_is_queens_turn of its own +; case. + +; WHAT IT DOES NOT CLAIM: it does not act: the page shows the move, it does not dispatch a bee. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_jam_action; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-jam-action"; +pub const TITLE : str = "The Queen's move: what each jam asks of her"; +pub const DESCRIPTION : str = "Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting."; +pub const IMAGE_ALT : str = "Jam action card: case 4, refused then pushed. The action reads dispatch a re-review. Ten case cards stand on two columns: seven in the gold QUEEN'S TURN column, none, draft or fresh and stacked on open in the grey WAIT column; case 4 is outlined."; +pub const HOOK : str = "An unknown case is never nothing to do: anything above 9 maps to measure again. A queue that cannot be read is not reported as free."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_action and jam_is_queens_turn, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 14 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CASES : u8 = 10; +pub const K_CASE_MAX : u8 = 255; +pub const K_PRESET_CASE : [5]u8 = [4, 1, 8, 6, 200]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Refused then pushed", "No reviewer", "Draft or fresh", "Conflicting", "Unknown case 200"]; +pub const SAY_ASK : str = "Which move is the Queen's for each jam, and which jams are not hers to move?"; +pub const SAY_CASE : str = "case"; +pub const SAY_CASE_NAMES : [10]str = ["none", "no reviewer", "stacked on open", "refused not fixed", "refused then pushed", "labeled not merged", "conflicting", "noise red", "draft or fresh", "unmeasured"]; +pub const SAY_ACTION_NAMES : [10]str = ["nothing", "dispatch a reviewer", "wait for the base", "dispatch the coder", "dispatch a re-review", "finish the merge", "rebase, by the coder", "name the noise and review", "wait", "measure again"]; +pub const SAY_COLUMNS : [2]str = ["QUEEN'S TURN", "WAIT"]; +pub const SAY_UNKNOWN : str = "unknown case {0}"; +pub const SAY_ACTION : str = "action"; +pub const SAY_NOTE : str = "A draft, a fresh PR and one stacked on an open PR are waited on, not pushed. Every other jam is a move the Queen makes herself, in the order of the board."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/review-jam.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_covers_unknown_cases { + assert K_CASE_MAX > K_CASES; + assert K_PRESET_CASE[4] > K_CASES; +} + +test the_start_is_refused_then_pushed { + assert K_PRESET_CASE[0] == 4; + assert K_PRESET_CASE[0] < K_CASES; +} diff --git a/apps/website/specs/widgets/hive-jam.t27 b/apps/website/specs/widgets/hive-jam.t27 new file mode 100644 index 0000000000..dfc818857d --- /dev/null +++ b/apps/website/specs/widgets/hive-jam.t27 @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-jam.t27 -- The review jam: which case a stuck pull request is in +; Source of truth for public/widgets/hive-jam/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-jam/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/review-jam.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 13 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader toggles the nine facts. The lit rung, where the pull request sits, is +; the case jam_case(measured, draft or fresh, stacked, conflicting, labeled, refused, pushed after, +; has reviewer, only noise red) names. + +; WHAT IT DOES NOT CLAIM: it reads no pull request: the facts are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_jam; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-jam"; +pub const TITLE : str = "The review jam: which case a stuck pull request is in"; +pub const DESCRIPTION : str = "Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule."; +pub const IMAGE_ALT : str = "Review jam card: a measured pull request, refused, pushed after the refusal, with a reviewer and only noise checks red. Of ten rules in first-match order the sixth, refused then pushed, is lit and holds the card, ahead of noise red, which also matches."; +pub const HOOK : str = "A pull request can be refused, pushed again and red at once. The earliest rule that matches wins, so the Queen always knows which one case she is in."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/review-jam.t27 (t27 master c42f8fb40): every value on the page is jam_case, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 13 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CASES : u8 = 10; +; The case jam_case returns when no rule matches; the rule order is read from jam_case itself (tool.js). +pub const K_JAM_NONE : u8 = 0; +pub const K_PRESET_FLAGS : [5]u16 = [481, 0, 3, 9, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Refused, pushed, noise red", "Not measured", "A draft", "Conflicting", "No reviewer yet"]; +pub const SAY_ASK : str = "A pull request has not moved for hours. Which jam is it in?"; +pub const SAY_FLAGS : [9]str = ["measured", "draft or fresh", "stacked on an open PR", "conflicting", "labeled reviewed", "refused", "pushed after the refusal", "has a reviewer", "only noise checks red"]; +pub const SAY_CASE_NAMES : [10]str = ["NONE", "NO REVIEWER", "STACKED ON OPEN", "REFUSED NOT FIXED", "REFUSED THEN PUSHED", "LABELED NOT MERGED", "CONFLICTING", "NOISE RED", "DRAFT OR FRESH", "UNMEASURED"]; +pub const SAY_RULES : str = "the rules, first match wins"; +pub const SAY_PR : str = "this pull request"; +pub const SAY_CASE : str = "case"; +pub const SAY_NOTE : str = "Pushed without a refusal is not refused then pushed: it is no jam at all. Unmeasured beats every other fact, because a pull request the Queen could not read is never reported as free."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/review-jam.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test ten_cases_ten_rules { + assert K_CASES == 10; + assert K_JAM_NONE == 0; + assert K_JAM_NONE < K_CASES; +} + +test the_start_is_measured_and_refused { + assert K_PRESET_FLAGS[0] == 481; + assert K_PRESET_FLAGS[1] == 0; +} diff --git a/apps/website/specs/widgets/hive-key-error.t27 b/apps/website/specs/widgets/hive-key-error.t27 new file mode 100644 index 0000000000..b8dccae6d5 --- /dev/null +++ b/apps/website/specs/widgets/hive-key-error.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-key-error.t27 -- A refused key: transient, quota, auth or a bad request +; Source of truth for public/widgets/hive-key-error/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-key-error/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 16 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a status and a code; the verdict is error_kind(status, code), with +; key_closes and charges_issue of its kind. Every grid cell is error_kind of its row and column. + +; WHAT IT DOES NOT CLAIM: it calls no provider: the statuses and codes are a list to pick from. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_key_error; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-key-error"; +pub const TITLE : str = "A refused key: transient, quota, auth or a bad request"; +pub const DESCRIPTION : str = "Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way."; +pub const IMAGE_ALT : str = "Key error card: HTTP 429 with z.ai code 1305. The verdict reads TRANSIENT in gold: the key rests, it does not close, the issue is not charged. A grid of 11 statuses by 7 codes is coloured gold for transient, orange for quota, pink for auth, red for request and green for ok."; +pub const HOOK : str = "z.ai sends a spent quota and a passing overload with the same 429. So the code in the body wins over the status, and 1305 never closes a key."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is error_kind, key_closes and charges_issue, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 16 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STATUSES : [11]u16 = [0, 200, 400, 401, 402, 403, 404, 408, 429, 500, 503]; +pub const K_CODES : [7]u16 = [0, 1113, 1302, 1305, 1308, 1312, 1321]; +pub const K_ZAI_OVERLOAD : u32 = 1305; +pub const K_ZAI_RATE_LIMIT : u32 = 1302; +pub const K_PRESET_STATUS : [4]u8 = [8, 8, 3, 0]; +pub const K_PRESET_CODE : [4]u8 = [3, 1, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["429, code 1305", "429, code 1113", "401", "No answer at all"]; +pub const SAY_ASK : str = "A bee's key was refused. Should it wait, switch keys, or give up?"; +pub const SAY_STATUS : str = "HTTP status (0: no answer)"; +pub const SAY_CODE : str = "z.ai code in the body (0: none)"; +pub const SAY_KIND_NAMES : [5]str = ["OK", "TRANSIENT", "QUOTA", "AUTH", "REQUEST"]; +pub const SAY_KIND_LINES : [5]str = ["answered", "another key or a moment later may pass", "the key's quota, plan or balance is spent", "the key itself is refused", "the request is wrong; every key would say the same"]; +pub const SAY_CLOSES : str = "closes the key"; +pub const SAY_CHARGES : str = "charged to the issue"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_GRID : str = "status by code: the kind error_kind gives"; +pub const SAY_NOTE : str = "Only a wrong request is charged to the issue: an overloaded, spent or refusing provider says nothing about the work, so it never retires an issue."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_lists_hold_the_overload_codes { + assert K_CODES[2] == K_ZAI_RATE_LIMIT; + assert K_CODES[3] == K_ZAI_OVERLOAD; + assert K_STATUSES[8] == 429; +} + +test the_presets_index_the_lists { + assert K_PRESET_STATUS[0] == 8; + assert K_PRESET_CODE[0] == 3; + assert K_STATUSES[0] == 0; +} diff --git a/apps/website/specs/widgets/hive-key-rest.t27 b/apps/website/specs/widgets/hive-key-rest.t27 new file mode 100644 index 0000000000..c8c768377a --- /dev/null +++ b/apps/website/specs/widgets/hive-key-rest.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-key-rest.t27 -- How long a refused key rests +; Source of truth for public/widgets/hive-key-rest/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-key-rest/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 17 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader picks a kind and the strikes in a row. Every rung is +; cooldown_seconds(transient, strikes); the dial and the verdict are cooldown_seconds of the picked +; kind. + +; WHAT IT DOES NOT CLAIM: it times nothing: the strikes are what the reader sets. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_key_rest; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-key-rest"; +pub const TITLE : str = "How long a refused key rests"; +pub const DESCRIPTION : str = "A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes."; +pub const IMAGE_ALT : str = "Key rest card: a transient refusal after 3 strikes. Rungs for 0 to 6 strikes read 15, 30, 60, 120, 240, 240, 240 seconds, the fourth lit. The dial shows 120 s against the 1800 s long break."; +pub const HOOK : str = "Long enough that the next bee does not hit the same overloaded key at once, short enough that 34 keys are never all resting through a one-minute burst."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is cooldown_seconds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 17 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STRIKES_MAX : u8 = 8; +pub const K_KE_TRANSIENT : u8 = 1; +pub const K_RUNGS : u8 = 7; +pub const K_COOL_BASE_SECONDS : u32 = 15; +pub const K_COOL_CAP_SECONDS : u32 = 240; +pub const K_KEY_BREAK_SECONDS : u32 = 1800; +pub const K_PRESET_KIND : [4]u8 = [1, 1, 2, 4]; +pub const K_PRESET_STRIKES : [4]u8 = [3, 6, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Transient, 3 strikes", "Transient, 6 strikes", "Quota spent", "A bad request"]; +pub const SAY_ASK : str = "How long should a key rest after it was refused?"; +pub const SAY_KIND : str = "kind of refusal"; +pub const SAY_KIND_NAMES : [5]str = ["ok", "transient", "quota", "auth", "request"]; +pub const SAY_STRIKES : str = "strikes in a row before this one"; +pub const SAY_RUNG : str = "after {0} in a row"; +pub const SAY_REST : str = "rests {0} s"; +pub const SAY_DIAL : str = "of the {0} s break"; +pub const SAY_NONE : str = "no rest"; +pub const SAY_NOTE : str = "The cap is the base doubled exactly four times, so the doubling meets 240 and never steps past it. A rest is always shorter than the break, so a 1305 can never close a key."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_rest_and_the_break { + assert K_COOL_BASE_SECONDS == 15; + assert K_COOL_CAP_SECONDS == 240; + assert K_KEY_BREAK_SECONDS == 1800; +} + +test the_rungs_fit_the_slider { + assert K_RUNGS <= K_STRIKES_MAX; + assert K_PRESET_STRIKES[1] < K_RUNGS; +} diff --git a/apps/website/specs/widgets/hive-lifecycle.t27 b/apps/website/specs/widgets/hive-lifecycle.t27 new file mode 100644 index 0000000000..81835c78f4 --- /dev/null +++ b/apps/website/specs/widgets/hive-lifecycle.t27 @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-lifecycle.t27 -- A task's life: the Queen's state machine +; Source of truth for public/widgets/hive-lifecycle/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-lifecycle/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/dispatch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 7 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps events; the state becomes next_state(state, event). Every event +; button is next_state of the current state, so an illegal one (99) is greyed and names nothing. +; Presets jump to a state. + +; WHAT IT DOES NOT CLAIM: it does not run the supervisor: the events are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_lifecycle; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-lifecycle"; +pub const TITLE : str = "A task's life: the Queen's state machine"; +pub const DESCRIPTION : str = "Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out."; +pub const IMAGE_ALT : str = "Lifecycle card: the task is ENDED, highlighted among twelve state chips. Of sixteen event buttons, seven are lit, the verdicts accept, send back, escalate, wait, empty, criteria changed and floor passed, each naming the state it leads to; the other nine are grey as illegal."; +pub const HOOK : str = "A row has no status column: its state is read from four fields. This machine is the one that chooses, starts, ends, reviews and releases work."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/dispatch.t27 (t27 master c42f8fb40): every value on the page is next_state, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 7 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_STATES : u8 = 12; +pub const K_EVENTS : u8 = 16; +pub const K_ILLEGAL : u32 = 99; +pub const K_PRESET_STATE : [4]u8 = [3, 0, 6, 10]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Ended, awaiting a verdict", "A new candidate", "Sent back", "Reaped"]; +pub const SAY_ASK : str = "What can happen to a task once a bee has started on it?"; +pub const SAY_STATE_NAMES : [12]str = ["none", "running", "refused", "ended", "wait", "accept", "send back", "escalate", "empty", "stale", "reaped", "released"]; +pub const SAY_EVENT_NAMES : [16]str = ["start", "refuse", "end", "end of another turn", "reap a stall", "reap at boot", "verdict accept", "verdict send back", "verdict escalate", "verdict wait", "verdict empty", "criteria changed", "CI red under the ceiling", "CI red at the ceiling", "floor passed", "dispatch again"]; +pub const SAY_STATES : str = "states"; +pub const SAY_EVENTS : str = "events: tap one"; +pub const SAY_NOW : str = "now"; +pub const SAY_ILLEGAL : str = "illegal here"; +pub const SAY_TRAIL : str = "path so far"; +pub const SAY_LEGAL : str = "{0} of {1} events are legal here"; +pub const SAY_NOTE : str = "Accepted is done only until CI goes red: a red check under the send-back ceiling sends it back, at the ceiling it escalates. A released or reaped task can be dispatched again."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/dispatch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test twelve_states_sixteen_events { + assert K_STATES == 12; + assert K_EVENTS == 16; + assert K_ILLEGAL > K_STATES; +} + +test the_presets_are_states { + assert K_PRESET_STATE[0] == 3; + assert K_PRESET_STATE[3] < K_STATES; +} diff --git a/apps/website/specs/widgets/hive-master-red.t27 b/apps/website/specs/widgets/hive-master-red.t27 new file mode 100644 index 0000000000..50aa86825e --- /dev/null +++ b/apps/website/specs/widgets/hive-master-red.t27 @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-master-red.t27 -- Inherited red: what master's runs say about a red check +; Source of truth for public/widgets/hive-master-red/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-master-red/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/merger_gate.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 15 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps each of four master runs to change its result and whether it ran at +; or after the PR's merge base, and whether the bee named the check. Each rung is run_says of its +; run and the running look_back from MASTER_UNKNOWN; the gate is discount_holds(bee named, the +; result). + +; WHAT IT DOES NOT CLAIM: it does not read GitHub Actions: the runs are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_master_red; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-master-red"; +pub const TITLE : str = "Inherited red: what master's runs say about a red check"; +pub const DESCRIPTION : str = "The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts."; +pub const IMAGE_ALT : str = "Inherited red card: four master runs, newest first: cancelled, failure, success, failure, all at or after the merge base. The top rung says unknown and is looked past, the next says red and decides; the look-back reads MASTER RED and the discount holds, green."; +pub const HOOK : str = "On 2026-10-06 a PR merged red: master's run was cancelled and read as red. A cancelled run says nothing about the code; only a completed failure counts."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/merger_gate.t27 (t27 master c42f8fb40): every value on the page is run_says, look_back and discount_holds, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 15 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_RUNS : u8 = 4; +pub const K_RUN_KINDS : u8 = 8; +pub const K_MASTER_UNKNOWN : u8 = 0; +pub const K_PRESET_R0 : [4]u8 = [3, 3, 3, 1]; +pub const K_PRESET_R1 : [4]u8 = [1, 3, 1, 1]; +pub const K_PRESET_R2 : [4]u8 = [2, 3, 2, 2]; +pub const K_PRESET_R3 : [4]u8 = [1, 3, 1, 1]; +pub const K_PRESET_BASE : [4]u8 = [15, 15, 1, 15]; +pub const K_PRESET_NAMED : [4]u8 = [1, 1, 1, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Cancelled, then a failure", "Only cancelled runs (#6956)", "Failure before the merge base", "Bee did not name it"]; +pub const SAY_ASK : str = "The bee says this red check is red on master too. Is it?"; +pub const SAY_RUN_NAMES : [8]str = ["missing", "failure", "success", "cancelled", "skipped", "timed out", "running", "other"]; +pub const SAY_MASTER_NAMES : [3]str = ["UNKNOWN", "MASTER RED", "MASTER GREEN"]; +pub const SAY_RUN : str = "master run {0}, newest first"; +pub const SAY_AFTER : str = "at or after the merge base"; +pub const SAY_BEFORE : str = "before the merge base"; +pub const SAY_TAP : str = "tap the result to change it"; +pub const SAY_NAMED : str = "the bee named this check"; +pub const SAY_SAYS : str = "says"; +pub const SAY_SO_FAR : str = "so far"; +pub const SAY_HOLDS : str = "The discount holds: the red is inherited"; +pub const SAY_FAILS : str = "No discount: the red blocks"; +pub const SAY_NOTE : str = "The newest decisive run wins: a cancelled newest run is looked past, a success is not. A run older than the merge base is not evidence about the code the PR started from."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/merger_gate.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_runs_eight_kinds { + assert K_RUNS == 4; + assert K_RUN_KINDS == 8; + assert K_MASTER_UNKNOWN == 0; +} + +test the_incident_preset_is_all_cancelled { + assert K_PRESET_R0[1] == 3; + assert K_PRESET_R3[1] == 3; + assert K_PRESET_BASE[0] == 15; +} diff --git a/apps/website/specs/widgets/hive-next-key.t27 b/apps/website/specs/widgets/hive-next-key.t27 new file mode 100644 index 0000000000..68f702abc5 --- /dev/null +++ b/apps/website/specs/widgets/hive-next-key.t27 @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-next-key.t27 -- The next key: a bee moves on instead of dying +; Source of truth for public/widgets/hive-next-key/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-next-key/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/key_failover.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 18 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the pool size, taps keys between free, tried and resting, and sets +; the attempts and how many keys answered 1305. The ring is next_free(tried, resting, size, last); +; the verdict is step(transient, attempts, that key, 1305 keys, size, the next pool) with +; pool_overloaded beside it. + +; WHAT IT DOES NOT CLAIM: it holds no key: the pool is a picture of masks, bit i is key i + 1. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_next_key; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-next-key"; +pub const TITLE : str = "The next key: a bee moves on instead of dying"; +pub const DESCRIPTION : str = "A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up."; +pub const IMAGE_ALT : str = "Next key card: a pool of 10 keys, key 9 just failed with a 1305 and is tried, keys 10 and 1 rest in red, the others are green. Key 2 has a white ring: next_free picked it. The verdict reads NEXT KEY at attempt 2 of 8."; +pub const HOOK : str = "Measured 2026-10-17: a bee died 7 s into its turn on key 9 of 10 while 33 keys stood by. Now it tries the next free key, never the one that failed."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/key_failover.t27 (t27 master c42f8fb40): every value on the page is next_free, pool_overloaded and step, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 18 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_SIZE_MAX : u8 = 12; +pub const K_KE_TRANSIENT : u8 = 1; +pub const K_ATTEMPTS_MAX : u8 = 9; +pub const K_OVERLOAD_MAX : u8 = 5; +pub const K_NO_KEY : u32 = 255; +pub const K_POOL_OVERLOAD_KEYS : u32 = 3; +pub const K_MAX_ATTEMPTS_PER_TURN : u32 = 8; +pub const K_PRESET_SIZE : [4]u8 = [10, 10, 10, 4]; +pub const K_PRESET_LAST : [4]u8 = [8, 8, 3, 0]; +pub const K_PRESET_TRIED : [4]u16 = [256, 256, 1023, 1]; +pub const K_PRESET_RESTING : [4]u16 = [513, 513, 0, 14]; +pub const K_PRESET_ATTEMPTS : [4]u8 = [2, 4, 8, 2]; +pub const K_PRESET_OVERLOAD : [4]u8 = [1, 3, 1, 1]; +pub const K_START_PRESET : u8 = 0; +; The pool list the bee falls back on: two pools, this one (pool 0) tried, none resting, this one +; failed last. next_free of it names the next pool. +pub const K_POOL_LIST_TRIED : u8 = 1; +pub const K_POOL_LIST_RESTING : u8 = 0; +pub const K_POOL_LIST_SIZE : u8 = 2; +pub const K_POOL_LIST_LAST : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Key 9 of 10 failed", "Three keys said 1305", "Every key tried", "A small pool"]; +pub const SAY_ASK : str = "A bee's key was refused mid-task. Which key does it try next?"; +pub const SAY_SIZE : str = "keys in the pool"; +pub const SAY_ATTEMPTS : str = "attempts this turn"; +pub const SAY_OVERLOAD : str = "keys that answered 1305 in the last minute"; +pub const SAY_KEY_STATES : [3]str = ["free", "tried", "resting"]; +pub const SAY_TAP : str = "tap a key: free, tried, resting"; +pub const SAY_LAST : str = "failed last"; +pub const SAY_STEP_NAMES : [5]str = ["DONE", "NEXT KEY", "NEXT POOL", "GIVE UP", "FAIL"]; +pub const SAY_NEXT : str = "next key"; +pub const SAY_NONE : str = "none left"; +pub const SAY_POOL_OUT : str = "pool overloaded"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_NOTE : str = "1305 is an overload of the pool's model, not of one key: once 3 distinct keys said it within a minute, the bee moves to the next pool instead of burning the rest. At 8 attempts the turn ends, and its claim is released."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/key_failover.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_bounds_are_the_specs { + assert K_NO_KEY == 255; + assert K_POOL_OVERLOAD_KEYS == 3; + assert K_MAX_ATTEMPTS_PER_TURN == 8; +} + +test the_pool_list_has_this_pool_tried { + assert K_POOL_LIST_SIZE == 2; + assert K_POOL_LIST_TRIED == 1; + assert K_POOL_LIST_LAST < K_POOL_LIST_SIZE; +} + +test the_start_pool_is_ten_keys { + assert K_PRESET_SIZE[0] == 10; + assert K_PRESET_LAST[0] < K_PRESET_SIZE[0]; + assert K_PRESET_TRIED[0] == 256; +} diff --git a/apps/website/specs/widgets/hive-night.t27 b/apps/website/specs/widgets/hive-night.t27 new file mode 100644 index 0000000000..911b0f9805 --- /dev/null +++ b/apps/website/specs/widgets/hive-night.t27 @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-night.t27 -- The night watch: four checks, one verdict, one report +; Source of truth for public/widgets/hive-night/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-night/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/card-chat-night-watch.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 25 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps each of four checks between pass, fail and unmeasured and says +; whether an issue is open. The host counts fails and unmeasured; the verdict is night_verdict of +; the counts and the column night_report(verdict, issue open). + +; WHAT IT DOES NOT CLAIM: it runs no check at night: the four results are what the reader taps. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_night; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-night"; +pub const TITLE : str = "The night watch: four checks, one verdict, one report"; +pub const DESCRIPTION : str = "Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent."; +pub const IMAGE_ALT : str = "Night watch card: four comb cells, thread green, status door red, merges green, parity green. The verdict reads FAIL; an issue is already open, so the report card sits in the COMMENT column of five: silent, open issue, comment, close issue, leave as is."; +pub const HOOK : str = "Any fail is a fail; else anything unmeasured leaves things as they are. A night that could not measure never closes an issue."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/card-chat-night-watch.t27 (t27 master c42f8fb40): every value on the page is night_verdict and night_report, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 25 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_CHECKS : u8 = 4; +pub const K_CARD_CHECK_VERDICTS : u8 = 4; +pub const K_PRESET_C0 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_C1 : [4]u8 = [1, 0, 1, 2]; +pub const K_PRESET_C2 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_C3 : [4]u8 = [0, 0, 0, 0]; +pub const K_PRESET_OPEN : [4]u8 = [1, 1, 0, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A fail, issue open", "All pass, issue open", "A fail, no issue", "Unmeasured"]; +pub const SAY_ASK : str = "The card checks ran overnight. What should the watch do in the morning?"; +pub const SAY_CHECK_NAMES : [4]str = ["thread", "status door", "merges", "parity"]; +pub const SAY_RESULTS : [3]str = ["pass", "fail", "unmeasured"]; +pub const SAY_TAP : str = "tap a check: pass, fail, unmeasured"; +pub const SAY_OPEN : str = "an issue is already open"; +pub const SAY_VERDICT_NAMES : [3]str = ["PASS", "FAIL", "UNMEASURED"]; +pub const SAY_REPORT_NAMES : [5]str = ["silent", "open an issue", "comment", "close the issue", "leave as is"]; +pub const SAY_TONIGHT : str = "tonight"; +pub const SAY_COUNTS : str = "{0} fail, {1} unmeasured"; +pub const SAY_NOTE : str = "A pass with an open issue closes it, so a fixed card does not leave a stale alarm. A night with nothing to say sends nothing."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/card-chat-night-watch.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test four_checks { + assert K_CHECKS == K_CARD_CHECK_VERDICTS; + assert K_CHECKS == 4; +} + +test the_start_has_one_fail { + assert K_PRESET_C1[0] == 1; + assert K_PRESET_OPEN[0] == 1; + assert K_PRESET_C3[3] == 0; +} diff --git a/apps/website/specs/widgets/hive-price.t27 b/apps/website/specs/widgets/hive-price.t27 new file mode 100644 index 0000000000..42332757b7 --- /dev/null +++ b/apps/website/specs/widgets/hive-price.t27 @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-price.t27 -- The price of an operation: twice its cost, rounded up +; Source of truth for public/widgets/hive-price/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-price/logic.js -- the fn +; bodies of gHashTag/t27 specs/automation/token-wallet.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 20 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the cost of one operation in micro-dollars and a monthly price in +; dollars. The price is price_tokens(cost); the month is month_tokens(dollars), and reels_in and +; portraits_in of that month. + +; WHAT IT DOES NOT CLAIM: it charges nothing; the reel and portrait sizes are the spec constants +; REEL_TOKENS and PORTRAIT_TOKENS. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_price; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-price"; +pub const TITLE : str = "The price of an operation: twice its cost, rounded up"; +pub const DESCRIPTION : str = "Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens."; +pub const IMAGE_ALT : str = "Price card: an operation that costs 12300 micro-dollars sells for 5 tokens. A $20 month buys 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12. The verdict line reads 5 tokens in gold."; +pub const HOOK : str = "One rate everywhere: a token is half a cent, and every operation is sold at twice what it costs, rounded up so nothing is sold below that."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/automation/token-wallet.t27 (t27 master c42f8fb40): every value on the page is price_tokens, month_tokens, reels_in and portraits_in, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 20 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_COST_MAX : u32 = 200000; +pub const K_COST_STEP : u16 = 100; +pub const K_USD_MAX : u8 = 200; +pub const K_TOKEN_MICRO_USD : u32 = 5000; +pub const K_MARKUP_PCT : u32 = 200; +pub const K_REEL_TOKENS : u32 = 270; +pub const K_PORTRAIT_TOKENS : u32 = 12; +pub const K_PRESET_COST : [4]u32 = [12300, 2500, 2600, 130000]; +pub const K_PRESET_USD : [4]u8 = [20, 49, 10, 100]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["A $0.0123 call, $20 a month", "Exactly half a cent", "Just over half a cent", "A $0.13 lip-sync, $100"]; +pub const SAY_ASK : str = "What does an operation cost a person, and what does a month buy?"; +pub const SAY_COST : str = "what it costs us, micro-dollars"; +pub const SAY_USD : str = "a month, dollars"; +pub const SAY_PRICE : str = "price, tokens"; +pub const SAY_MONTH : str = "tokens a month"; +pub const SAY_REELS : str = "reels in a month"; +pub const SAY_PORTRAITS : str = "portraits in a month"; +pub const SAY_COST_DOLLARS : str = "costs ${0}"; +pub const SAY_EACH : str = "{0} tokens"; +pub const SAY_NOTE : str = "2500 micro-dollars is half a cent: at twice the cost that is exactly one token. 2600 is two, because the price rounds up. Tokens not spent within a month burn: the next lesson."; +pub const SAY_SOURCE : str = "Logic: t27 spec automation/token-wallet.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test one_rate_one_markup { + assert K_TOKEN_MICRO_USD == 5000; + assert K_MARKUP_PCT == 200; +} + +test the_presets_fit { + assert K_PRESET_COST[3] <= K_COST_MAX; + assert K_PRESET_USD[3] <= K_USD_MAX; + assert K_PRESET_COST[1] * 2 == K_TOKEN_MICRO_USD; +} diff --git a/apps/website/specs/widgets/hive-rank.t27 b/apps/website/specs/widgets/hive-rank.t27 new file mode 100644 index 0000000000..e9d49fa470 --- /dev/null +++ b/apps/website/specs/widgets/hive-rank.t27 @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-rank.t27 -- The queue: which issue the Queen takes first +; Source of truth for public/widgets/hive-rank/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-rank/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/priority.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 5 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: seven issues with fixed ages and blockers, in listing order; the reader taps a +; level for each. The host counts the criticals listed before each one and then calls capped_level, +; effective_level, eligible and why; the order is a sort whose only comparison is outranks. + +; WHAT IT DOES NOT CLAIM: the seven issues are fixtures of this page, not the live queue. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_rank; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-rank"; +pub const TITLE : str = "The queue: which issue the Queen takes first"; +pub const DESCRIPTION : str = "Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there."; +pub const IMAGE_ALT : str = "Queue card: seven issues ranked. Three criticals lead, the fourth critical is capped to high, an unlabelled normal comes next, a low issue aged to normal follows it, and one high issue with an open blocker is skipped. Each row carries a why pill: label, capped, listing, aged, blocked."; +pub const HOOK : str = "Labels can inflate: at most 3 criticals run as critical, the fourth runs as high. An issue with an open blocker is skipped, not waited on."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/priority.t27 (t27 master c42f8fb40): every value on the page is capped_level, effective_level, eligible, outranks and why, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 5 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_ISSUES : u8 = 7; +pub const K_CRITICAL_CAP : u32 = 3; +pub const K_LEVEL_CRITICAL : u8 = 0; +pub const K_LEVEL_NORMAL : u8 = 2; +pub const K_LEVEL_LOW : u8 = 3; +; The tap state past the lowest level: the issue carries no priority label (SAY_LEVEL_NAMES[4]). +pub const K_NO_LABEL : u8 = 4; +pub const K_AGES : [7]u8 = [1, 3, 6, 8, 20, 30, 2]; +pub const K_BLOCKERS : [7]u8 = [0, 0, 0, 0, 0, 0, 1]; +pub const K_NUMBERS : [7]u16 = [712, 706, 699, 690, 681, 664, 710]; +pub const K_PRESET_L0 : [3]u8 = [0, 1, 0]; +pub const K_PRESET_L1 : [3]u8 = [0, 2, 0]; +pub const K_PRESET_L2 : [3]u8 = [0, 4, 0]; +pub const K_PRESET_L3 : [3]u8 = [0, 4, 0]; +pub const K_PRESET_L4 : [3]u8 = [4, 4, 4]; +pub const K_PRESET_L5 : [3]u8 = [3, 3, 3]; +pub const K_PRESET_L6 : [3]u8 = [1, 4, 1]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["Four criticals", "Few labels", "Two criticals"]; +pub const SAY_ASK : str = "Which open issue does the Queen take first?"; +pub const SAY_LEVEL_NAMES : [5]str = ["critical", "high", "normal", "low", "no label"]; +pub const SAY_WHY_NAMES : [5]str = ["listing", "label", "aged", "capped", "blocked"]; +pub const SAY_ISSUE : str = "#{0} ({1} d open)"; +pub const SAY_TAP : str = "Tap an issue to change its label"; +pub const SAY_ORDER : str = "The order the Queen takes them"; +pub const SAY_SKIPPED : str = "skipped: an open blocker"; +pub const SAY_RUNS : str = "runs as {0}"; +pub const SAY_FIRST : str = "taken first"; +pub const SAY_CAPPED : str = "capped"; +pub const SAY_NOTE : str = "Lower level first, then the real level (an aged issue never beats a real one of its level), then GitHub listing order. An unlabelled issue keeps the place it had before labels existed."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/priority.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test no_label_is_the_state_past_the_lowest_level { + assert K_NO_LABEL == K_LEVEL_LOW + 1; + assert K_LEVEL_CRITICAL < K_LEVEL_NORMAL; +} + +test seven_issues_in_listing_order { + assert K_ISSUES == 7; + assert K_AGES[6] == 2; + assert K_BLOCKERS[6] == 1; +} + +test the_start_has_one_critical_more_than_the_cap { + assert K_PRESET_L3[0] == 0; + assert K_CRITICAL_CAP == 3; + assert K_PRESET_L4[0] == 4; +} diff --git a/apps/website/specs/widgets/hive-review-first.t27 b/apps/website/specs/widgets/hive-review-first.t27 new file mode 100644 index 0000000000..e3396a37e9 --- /dev/null +++ b/apps/website/specs/widgets/hive-review-first.t27 @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-review-first.t27 -- Review first: no new work while finished work waits +; Source of truth for public/widgets/hive-review-first/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-review-first/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/review_first.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 6 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader drags the review queue from 0 to 40. Four candidates are +; kind_of(assigned, sent back) for each pair of flags, and each sits in the column admission(kind, +; queue) names; the sentence shows when round_refused says a round that admitted only the backlog +; owes it. + +; WHAT IT DOES NOT CLAIM: it does not count a live queue: 29 is the reviewable count the spec +; records for 2026-10-18 17:09Z. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_review_first; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-review-first"; +pub const TITLE : str = "Review first: no new work while finished work waits"; +pub const DESCRIPTION : str = "Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go."; +pub const IMAGE_ALT : str = "Review-first card at 29 dispatches waiting in review. The green ADMIT column holds three cards, sent back, assigned, and assigned and sent back; the red REVIEW FIRST column holds the backlog issue. The line under them reads 29 dispatches wait in review; the backlog is closed until the review queue is empty."; +pub const HOOK : str = "Measured 2026-10-18: nine bees started for every verdict, so the review queue only grew. Now the backlog closes while anything waits in review."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/review_first.t27 (t27 master c42f8fb40): every value on the page is kind_of, admission and round_refused, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 6 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_QUEUE_MAX : u8 = 40; +pub const K_REVIEW_QUEUE_MAX : u32 = 0; +pub const K_PRESET_QUEUE : [3]u8 = [29, 0, 3]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [3]str = ["29 waiting, as measured", "Review queue empty", "3 waiting"]; +pub const SAY_ASK : str = "Should the Queen start new work while finished work waits for a review?"; +pub const SAY_QUEUE : str = "dispatches waiting in review"; +pub const SAY_COLUMNS : [2]str = ["ADMIT", "REVIEW FIRST"]; +pub const SAY_CANDIDATES : [4]str = ["a backlog issue", "sent back by the review", "assigned by the owner", "assigned and sent back"]; +pub const SAY_KIND_NAMES : [3]str = ["backlog", "rework", "assigned"]; +pub const SAY_TAIL : str = " dispatches wait in review; the backlog is closed until the review queue is empty"; +pub const SAY_OPEN : str = "The backlog is open: the queue is empty."; +pub const SAY_ROUND : str = "a round with only backlog candidates says"; +pub const SAY_NOTE : str = "Rework is never backlog: the review asked for it. The queue empties on its own, because every outcome of a review that is not an accept has a clock (review_valve.t27)."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/review_first.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_owners_bound_is_zero { + assert K_REVIEW_QUEUE_MAX == 0; + assert K_PRESET_QUEUE[1] == K_REVIEW_QUEUE_MAX; +} + +test the_start_is_the_measured_queue { + assert K_PRESET_QUEUE[0] == 29; + assert K_PRESET_QUEUE[0] <= K_QUEUE_MAX; +} diff --git a/apps/website/specs/widgets/hive-reviewers.t27 b/apps/website/specs/widgets/hive-reviewers.t27 new file mode 100644 index 0000000000..7c48d0930d --- /dev/null +++ b/apps/website/specs/widgets/hive-reviewers.t27 @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-reviewers.t27 -- How many reviewer bees: keeping up with the finish rate +; Source of truth for public/widgets/hive-reviewers/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-reviewers/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 10 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets keys, lanes per key, free memory, memory per review, rows waiting +; and rows finished an hour. The upper meter is reviewer_concurrency of the top five, the lower +; slots_needed(finished an hour), and the gate keeps_up of the two. + +; WHAT IT DOES NOT CLAIM: the start numbers are the ones the spec records for 2026-10-09; the page +; measures nothing. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_reviewers; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-reviewers"; +pub const TITLE : str = "How many reviewer bees: keeping up with the finish rate"; +pub const DESCRIPTION : str = "Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work."; +pub const IMAGE_ALT : str = "Reviewer card at the load of 2026-10-09: 111 rows waiting, 58 finished an hour, 4 keys of 1 lane. Meters read 4 reviews at once against 5 needed; the gate is shut, the reviewer falls behind. The fixed bound of 4 is marked on the meter."; +pub const HOOK : str = "At 300 s a review, one slot clears 12 rows an hour. The fixed 4 tops out at 48 while bees finish 58, so the backlog only grew. 16 lanes keep up."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is reviewer_concurrency, slots_needed and keeps_up, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 10 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KEYS_MAX : u8 = 20; +pub const K_LANES_MAX : u8 = 4; +pub const K_MB_MAX : u16 = 8000; +pub const K_MB_STEP : u16 = 250; +pub const K_PER_MAX : u16 = 1000; +pub const K_PER_STEP : u8 = 50; +pub const K_WAITING_MAX : u8 = 150; +pub const K_RATE_MAX : u8 = 120; +pub const K_REVIEWER_CONCURRENCY : u32 = 4; +pub const K_REVIEWER_CEILING : u32 = 16; +pub const K_REVIEW_ROW_SECONDS : u32 = 300; +pub const K_PRESET_KEYS : [4]u8 = [4, 10, 4, 4]; +pub const K_PRESET_LANES : [4]u8 = [1, 2, 1, 1]; +pub const K_PRESET_MB : [4]u16 = [8000, 8000, 1000, 8000]; +pub const K_PRESET_PER : [4]u16 = [500, 500, 500, 500]; +pub const K_PRESET_WAITING : [4]u8 = [111, 111, 111, 0]; +pub const K_PRESET_RATE : [4]u8 = [58, 58, 58, 58]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["As measured, 4 keys", "10 keys of 2 lanes", "Short of memory", "Nothing waits"]; +pub const SAY_ASK : str = "How many reviews must run at once so finished work does not pile up?"; +pub const SAY_KEYS : str = "model keys"; +pub const SAY_LANES : str = "lanes per key"; +pub const SAY_MB : str = "free memory, MB"; +pub const SAY_PER : str = "MB per review"; +pub const SAY_WAITING : str = "rows waiting"; +pub const SAY_RATE : str = "rows finished an hour"; +pub const SAY_AT_ONCE : str = "reviews at once"; +pub const SAY_NEEDED : str = "needed to keep up"; +pub const SAY_FIXED : str = "the fixed bound of {0}"; +pub const SAY_KEEPS : str = "The reviewer keeps up"; +pub const SAY_BEHIND : str = "The reviewer falls behind"; +pub const SAY_GAP : str = "{0} at once, {1} needed"; +pub const SAY_NOTE : str = "0 only when nothing waits, at least 1 while anything waits, never above 16. Each review takes a lane no bee holds and runs on its own clock, so the round no longer reviews at all."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/reviewer.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_bounds_are_the_specs { + assert K_REVIEWER_CONCURRENCY == 4; + assert K_REVIEWER_CEILING == 16; + assert K_REVIEW_ROW_SECONDS == 300; +} + +test the_start_is_the_measured_load { + assert K_PRESET_WAITING[0] == 111; + assert K_PRESET_RATE[0] == 58; + assert K_PRESET_KEYS[0] == K_REVIEWER_CONCURRENCY; +} diff --git a/apps/website/specs/widgets/hive-revisit.t27 b/apps/website/specs/widgets/hive-revisit.t27 new file mode 100644 index 0000000000..d85c6310a9 --- /dev/null +++ b/apps/website/specs/widgets/hive-revisit.t27 @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-revisit.t27 -- Back off: when the reviewer looks at a waiting row again +; Source of truth for public/widgets/hive-revisit/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-revisit/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/reviewer.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 11 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the waits in a row and the seconds since the last visit. Each bar +; is revisit_after_seconds of its wait count; the dial and the verdict are due_again(waits, +; seconds). + +; WHAT IT DOES NOT CLAIM: the 941 and 888 are the spec record of 2026-10-09; the page visits +; nothing. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_revisit; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-revisit"; +pub const TITLE : str = "Back off: when the reviewer looks at a waiting row again"; +pub const DESCRIPTION : str = "A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due."; +pub const IMAGE_ALT : str = "Revisit card: a row that answered wait 4 times, 200 s after its last visit. Bars for 0 to 8 waits read 0, 30, 60, 120, 240, 480, 900, 900, 900 seconds, the fifth lit. The clock dial fills to 200 of 240: not due yet."; +pub const HOOK : str = "Measured over 44 minutes: 941 visits, 888 of them wait, from 22 rows. One row was visited 54 times. Doubling the pause gave those slots back."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/reviewer.t27 (t27 master c42f8fb40): every value on the page is revisit_after_seconds and due_again, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 11 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_WAITS_MAX : u8 = 8; +pub const K_SINCE_MAX : u16 = 1000; +pub const K_SINCE_STEP : u8 = 10; +pub const K_WAIT_REVISIT_BASE_SECONDS : u32 = 30; +pub const K_WAIT_REVISIT_CAP_SECONDS : u32 = 900; +pub const K_PRESET_WAITS : [4]u8 = [4, 1, 7, 0]; +pub const K_PRESET_SINCE : [4]u16 = [200, 40, 600, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Four waits, 200 s", "One wait, 40 s", "Seven waits, at the cap", "A fresh row"]; +pub const SAY_ASK : str = "When should the reviewer look again at a row that keeps answering wait?"; +pub const SAY_WAITS : str = "waits in a row"; +pub const SAY_SINCE : str = "seconds since the last visit"; +pub const SAY_BARS : str = "pause before the next visit, by waits in a row"; +pub const SAY_DUE : str = "due again"; +pub const SAY_NOT_DUE : str = "not due yet"; +pub const SAY_PAUSE : str = "pause, s"; +pub const SAY_NOTE : str = "Any answer other than wait resets the count, so a row that starts to read is visited at the normal pace again. The cap keeps a stuck row in sight several times an hour."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/reviewer.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_pause_starts_at_30_and_stops_at_900 { + assert K_WAIT_REVISIT_BASE_SECONDS == 30; + assert K_WAIT_REVISIT_CAP_SECONDS == 900; + assert K_SINCE_MAX > K_WAIT_REVISIT_CAP_SECONDS; +} + +test the_presets_fit { + assert K_PRESET_WAITS[2] <= K_WAITS_MAX; + assert K_PRESET_SINCE[2] <= K_SINCE_MAX; +} diff --git a/apps/website/specs/widgets/hive-shape.t27 b/apps/website/specs/widgets/hive-shape.t27 new file mode 100644 index 0000000000..64800a668d --- /dev/null +++ b/apps/website/specs/widgets/hive-shape.t27 @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-shape.t27 -- Shaping: what the Queen does with an issue that is not ready +; Source of truth for public/widgets/hive-shape/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-shape/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/shaping.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 1 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets the sections judge() finds missing, whether the issue is an epic, +; the attempts so far, whether the author was told, and the minutes since the last attempt. The card +; goes to the column shape_action names; the row of attempts 0 to 4 is shape_action at each count; +; the tile is attempt_due. + +; WHAT IT DOES NOT CLAIM: it does not run the model that writes the sections, nor judge() itself +; (specs/policy/issue_shape.t27). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_shape; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-shape"; +pub const TITLE : str = "Shaping: what the Queen does with an issue that is not ready"; +pub const DESCRIPTION : str = "Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing."; +pub const IMAGE_ALT : str = "Shaping card: an issue missing two sections after two failed attempts. The board puts its card in the gold TELL column; the attempt row reads shape, shape, tell, tell, tell for attempts 0 to 4. Tiles read action TELL, attempt due yes after 420 minutes, limit 2."; +pub const HOOK : str = "Two failed shapes and the Queen stops rewriting the issue: she comments once with what is missing, and never again."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/shaping.t27 (t27 master c42f8fb40): every value on the page is shape_action and attempt_due, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 1 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MISSING_MAX : u8 = 3; +pub const K_ATTEMPTS_MAX : u8 = 4; +pub const K_MINUTES_MAX : u16 = 720; +pub const K_MINUTES_STEP : u8 = 30; +pub const K_SHAPE_ATTEMPT_LIMIT : u32 = 2; +pub const K_SHAPE_RETRY_MINUTES : u32 = 360; +pub const K_PRESET_MISSING : [5]u8 = [2, 0, 2, 1, 2]; +pub const K_PRESET_CONTAINER : [5]u8 = [0, 0, 1, 0, 0]; +pub const K_PRESET_ATTEMPTS : [5]u8 = [2, 0, 0, 0, 3]; +pub const K_PRESET_TOLD : [5]u8 = [0, 0, 0, 0, 1]; +pub const K_PRESET_MINUTES : [5]u16 = [420, 0, 0, 0, 600]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Two shapes failed", "A ready issue", "An epic", "A first try", "Already told"]; +pub const SAY_ASK : str = "What does the Queen do with an issue that is not ready for a bee?"; +pub const SAY_MISSING : str = "sections missing"; +pub const SAY_CONTAINER : str = "an epic or roadmap"; +pub const SAY_ATTEMPTS : str = "shapes tried"; +pub const SAY_TOLD : str = "author already told"; +pub const SAY_MINUTES : str = "minutes since the last try"; +pub const SAY_ACTIONS : [3]str = ["LEAVE", "SHAPE", "TELL"]; +pub const SAY_ACTION_LINES : [3]str = ["nothing to do", "write the missing sections, judge them", "comment once with what is missing"]; +pub const SAY_ISSUE : str = "the issue"; +pub const SAY_ROW : str = "the same issue after 0, 1, 2, 3 and 4 tries"; +pub const SAY_ACTION : str = "action"; +pub const SAY_DUE : str = "another try due"; +pub const SAY_YES : str = "yes"; +pub const SAY_NO : str = "no"; +pub const SAY_LIMIT : str = "attempt limit"; +pub const SAY_NOTE : str = "A shape is one model call, and a refused shape is not retried for 360 minutes. After the limit the Queen stops writing and comments once, so a broken issue never costs a model call every round."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/shaping.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_presets_stay_in_range { + assert K_PRESET_MISSING[0] <= K_MISSING_MAX; + assert K_PRESET_ATTEMPTS[4] <= K_ATTEMPTS_MAX; + assert K_PRESET_MINUTES[4] <= K_MINUTES_MAX; +} + +test the_start_is_past_the_limit { + assert K_PRESET_ATTEMPTS[0] == K_SHAPE_ATTEMPT_LIMIT; + assert K_PRESET_MISSING[0] > 0; + assert K_PRESET_TOLD[0] == 0; +} diff --git a/apps/website/specs/widgets/hive-ship.t27 b/apps/website/specs/widgets/hive-ship.t27 new file mode 100644 index 0000000000..264087dfd5 --- /dev/null +++ b/apps/website/specs/widgets/hive-ship.t27 @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-ship.t27 -- The game: where a bee flies and how it leaves +; Source of truth for public/widgets/hive-ship/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-ship/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/hive.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 8 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader sets queued, working, the cell (placed, pending, none), departing, and +; the milliseconds since it left. The ship is drawn where ship_mode says, at the opacity ship_alpha +; gives in thousandths. + +; WHAT IT DOES NOT CLAIM: it is a flat sketch of the 3D game at app.t27.ai/game, not the game. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_ship; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-ship"; +pub const TITLE : str = "The game: where a bee flies and how it leaves"; +pub const DESCRIPTION : str = "Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out."; +pub const IMAGE_ALT : str = "Ship card: a map with the hive entrance on the left, a repository portal in the middle and an issue cell on the right. The bee has left the feed 1200 ms ago: it flies HOME along the path at half opacity, 500 of 1000. Tiles read mode HOME, opacity 500, fade 2400 ms."; +pub const HOOK : str = "Before this spec a bee that left the feed vanished in the frame it left. Now it flies home and fades over 2400 ms, so a finished task has an ending on screen."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/hive.t27 (t27 master c42f8fb40): every value on the page is ship_mode and ship_alpha, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 8 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_MS_MAX : u16 = 3000; +pub const K_MS_STEP : u8 = 100; +pub const K_DEPART_MS : u32 = 2400; +pub const K_PRESET_QUEUED : [5]u8 = [0, 0, 0, 1, 0]; +pub const K_PRESET_WORKING : [5]u8 = [1, 1, 0, 0, 1]; +pub const K_PRESET_CELL : [5]u8 = [0, 0, 0, 0, 1]; +pub const K_PRESET_DEPARTING : [5]u8 = [1, 0, 0, 0, 0]; +pub const K_PRESET_MS : [5]u16 = [1200, 0, 0, 0, 0]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [5]str = ["Task over, flying home", "Working on its cell", "Silent too long", "Not claimed yet", "Cell not placed"]; +pub const SAY_ASK : str = "Where does a bee fly in the game, and what happens when its task ends?"; +pub const SAY_QUEUED : str = "not claimed yet"; +pub const SAY_WORKING : str = "heard from lately"; +pub const SAY_CELL : str = "its issue cell"; +pub const SAY_CELL_NAMES : [3]str = ["placed", "pending", "none"]; +pub const SAY_DEPARTING : str = "left the feed"; +pub const SAY_MS : str = "ms since it left"; +pub const SAY_MODE_SHORT : [5]str = ["PARK", "ORBIT", "HOVER", "PORTAL", "HOME"]; +pub const SAY_MODE_NAMES : [5]str = ["PARK at the entrance", "ORBIT its cell", "HOVER at the rim", "PORTAL of its repository", "HOME, fading"]; +pub const SAY_PLACES : [3]str = ["entrance", "portal", "issue cell"]; +pub const SAY_MAP : str = "the map: entrance, portal, cell"; +pub const SAY_MODE : str = "mode"; +pub const SAY_ALPHA : str = "opacity of 1000"; +pub const SAY_FADE : str = "fade, ms"; +pub const SAY_NOTE : str = "A quiet bee hovers instead of orbiting: one that has not spoken for minutes is not drawn as busy. A bee whose cell is not on the map yet waits at its repository portal, so where it is going is already true."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/hive.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_slider_covers_the_fade { + assert K_MS_MAX > K_DEPART_MS; + assert K_DEPART_MS == 2400; +} + +test the_start_is_halfway_home { + assert K_PRESET_DEPARTING[0] == 1; + assert K_PRESET_MS[0] * 2 == K_DEPART_MS; +} diff --git a/apps/website/specs/widgets/hive-task-filter.t27 b/apps/website/specs/widgets/hive-task-filter.t27 new file mode 100644 index 0000000000..771bb5bc61 --- /dev/null +++ b/apps/website/specs/widgets/hive-task-filter.t27 @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/hive-task-filter.t27 -- One task, three kinds: the filter the board and the game share +; Source of truth for public/widgets/hive-task-filter/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English and NO RULE of its +; own: every column, verdict and number it shows comes from public/widgets/hive-task-filter/logic.js -- the fn +; bodies of gHashTag/t27 specs/queen/tasks.t27 compiled to wasm, beside its copy logic.t27. +; Lesson 3 of the course queen-and-hive. ASCII only (L3), English only (LANG-EN). +; A K_ constant is an input, a preset, a range or a constant of the logic spec (widget-logic.mjs holds +; a K_ whose name the logic spec also has equal to it); never the result of a spec fn. + +; WHAT IT DOES: the reader taps kinds and states into two masks; every cell of the 3 x 7 grid is +; task_matches(kind mask, state mask, kind, state). The two rows below map a job state and a review +; state to a task state with job_state and review_state. + +; WHAT IT DOES NOT CLAIM: it does not read the live board: the grid is every kind against every +; state. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_hive_task_filter; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "hive-task-filter"; +pub const TITLE : str = "One task, three kinds: the filter the board and the game share"; +pub const DESCRIPTION : str = "An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through."; +pub const IMAGE_ALT : str = "Task filter card: kinds review and job and states running and failed are pressed. A 3 by 7 grid lights four cells, review and job in running and failed, and leaves the rest dark. Below, a job running, done, failed, cancelled reads running, done, failed, dropped."; +pub const HOOK : str = "An issue, a job and a review each kept their own codes. Now one filter, a bit per kind and a bit per state, serves the board, the game and the route."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [2]str = ["gHashTag/t27 specs/queen/tasks.t27 (t27 master c42f8fb40): every value on the page is task_matches, job_state and review_state, its fn bodies compiled to wasm (logic.js)", "Course 11 of t27.ai, queen-and-hive (gHashTag/trinity#1698): lesson 3 opens this widget"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers: ranges, presets, and constants the logic spec holds ---------------------------- +pub const K_KINDS : u8 = 3; +pub const K_STATES : u8 = 7; +pub const K_TASK_KINDS : u8 = 3; +pub const K_TASK_STATES : u8 = 7; +pub const K_PRESET_KINDS : [4]u8 = [6, 0, 1, 0]; +pub const K_PRESET_STATES : [4]u8 = [68, 0, 4, 8]; +pub const K_START_PRESET : u8 = 0; + +; --- Words on screen ------------------------------------------------------------------------ +pub const SAY_PRESETS_LABEL : str = "Start from"; +pub const SAY_PRESET_NAMES : [4]str = ["Reviews and jobs, running or failed", "Everything (0 means any)", "Issues running", "Anything in review"]; +pub const SAY_ASK : str = "How can one board show issues, reviews and jobs at once?"; +pub const SAY_KIND_NAMES : [3]str = ["issue", "review", "job"]; +pub const SAY_STATE_NAMES : [7]str = ["backlog", "blocked", "running", "review", "done", "dropped", "failed"]; +pub const SAY_KINDS : str = "kinds"; +pub const SAY_STATES : str = "states"; +pub const SAY_GRID : str = "kind by state: lit means the filter lets it through"; +pub const SAY_JOB : str = "a job that is"; +pub const SAY_JOB_STATES : [4]str = ["running", "done", "failed", "cancelled"]; +pub const SAY_REVIEW : str = "a review row that is"; +pub const SAY_REVIEW_STATES : [4]str = ["queued", "posted", "forgotten", "failed"]; +pub const SAY_SHOWS : str = "shows as"; +pub const SAY_PASS : str = "in"; +pub const SAY_NOTE : str = "A mask of 0 means any. A job and a review keep their own machines; this spec only says which of the seven states each one shows as, so the board, the game and the route filter alike."; +pub const SAY_SOURCE : str = "Logic: t27 spec queen/tasks.t27, compiled"; + +test the_card_is_the_share_size { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; + assert K_CARD_WIDTH * 21 == K_CARD_HEIGHT * 40; +} + +test the_grid_is_every_kind_by_every_state { + assert K_KINDS == K_TASK_KINDS; + assert K_STATES == K_TASK_STATES; + assert K_KINDS * K_STATES == 21; +} + +test the_masks_fit_their_bits { + assert K_PRESET_KINDS[0] < 8; + assert K_PRESET_STATES[0] < 128; + assert K_PRESET_KINDS[1] == 0; +} diff --git a/apps/website/src/components/Navigation.tsx b/apps/website/src/components/Navigation.tsx index e69a7da76b..898dd77cc2 100644 --- a/apps/website/src/components/Navigation.tsx +++ b/apps/website/src/components/Navigation.tsx @@ -55,6 +55,7 @@ const PAGES: PageLink[] = [ { href: '#/ternary-computing', en: 'Course: the ternary machine', ru: 'Курс: троичная машина', note: 'Trits, logic, adders, a machine, neurons; a live widget per lesson, 27 lessons', noteRu: 'Триты, логика, сумматоры, машина, нейроны; живой виджет в каждом уроке, 27 уроков' }, { href: '#/arithmetic-and-dsp', en: 'Course: arithmetic and DSP', ru: 'Курс: арифметика и DSP', note: 'Adders, multipliers, fixed point, CORDIC, FIR, CIC, FFT, 27 lessons', noteRu: 'Сумматоры, умножители, фиксированная точка, CORDIC, FIR, CIC, БПФ, 27 уроков' }, { href: '#/neural-networks', en: 'Course: neural networks', ru: 'Курс: нейросети', note: 'Activations, losses, Adam, backprop, XOR, attention, a transformer block, 27 lessons', noteRu: 'Активации, потери, Adam, обратное распространение, XOR, внимание, блок трансформера, 27 уроков' }, + { href: '#/queen-and-hive', en: 'Course: the Queen and the hive', ru: 'Курс: Королева и улей', note: 'How the project builds itself: tasks, review, keys, the board, 27 lessons', noteRu: 'Как проект строит себя сам: задачи, ревью, ключи, доска, 27 уроков' }, { href: '#/foundry', en: 'Golden Foundry', ru: 'Золотая Литейная', note: 'A club for people who build on silicon', noteRu: 'Клуб разработчиков на кремнии', color: '#C9A24B' }, { href: '#/about', en: 'About', ru: 'Об авторе', note: 'Background, papers, contact', noteRu: 'Биография, статьи, контакты' }, { href: '#/resources', en: 'Resources', ru: 'Материалы', note: 'Papers and datasets, each with a DOI', noteRu: 'Статьи и датасеты, у каждого DOI' }, diff --git a/apps/website/src/data/blog/bodies/the-queen-and-the-hive-a-course-on-how-the-project-builds-itself.ts b/apps/website/src/data/blog/bodies/the-queen-and-the-hive-a-course-on-how-the-project-builds-itself.ts new file mode 100644 index 0000000000..a18aa3a82a --- /dev/null +++ b/apps/website/src/data/blog/bodies/the-queen-and-the-hive-a-course-on-how-the-project-builds-itself.ts @@ -0,0 +1,236 @@ +import type { Block } from '../types' + +// Course 11, queen-and-hive: 27 widgets, each running the fn bodies of a gHashTag/t27 specs/queen or +// specs/automation spec compiled to wasm (999 scripts/t27-logic.mjs, then scripts/widget-logic.mjs). The +// figure was drawn from the compiled review-jam.t27 the hive-jam widget runs: jam_case over all 512 +// combinations of its 9 facts, coloured by jam_is_queens_turn. + +export const body: Block[] = [ + { + "kind": "p", + "text": "Course 11 of t27.ai is out: the Queen and the hive. It tells how this project builds itself. An issue becomes a task for a bee, the bee writes a .t27 spec, a reviewer bee checks it, it merges, and the site regenerates. The Queen who runs that loop is not a black box: her rules live in gHashTag/t27 specs/queen and specs/automation as specs with numeric tests. The course has 27 lessons in 9 modules, each opening with a question, and each widget answers it with the spec's own fn bodies compiled to WebAssembly." + }, + { + "kind": "h", + "text": "Nine modules, twenty-seven widgets" + }, + { + "kind": "table", + "head": [ + "Module", + "Widgets", + "Specs" + ], + "rows": [ + [ + "The task", + "hive-shape, hive-boundary, hive-task-filter", + "queen/shaping, queen/tasks" + ], + [ + "Who goes next", + "hive-aging, hive-rank, hive-review-first", + "queen/priority, queen/review_first" + ], + [ + "A bee's life", + "hive-lifecycle, hive-ship, hive-drain", + "queen/dispatch, queen/hive, queen/drain" + ], + [ + "The reviewer bee", + "hive-reviewers, hive-revisit, hive-gate", + "queen/reviewer, queen/merger_gate" + ], + [ + "The review jam", + "hive-jam, hive-jam-action, hive-master-red", + "automation/review-jam, queen/merger_gate" + ], + [ + "Keys", + "hive-key-error, hive-key-rest, hive-next-key", + "queen/key_failover" + ], + [ + "Credit and money", + "hive-credit, hive-price, hive-burn", + "automation/queen-contributor-keys, automation/token-wallet" + ], + [ + "Agents among themselves", + "hive-called, hive-chain, hive-cooldown", + "automation/agent-hive-group" + ], + [ + "Night and the board", + "hive-night, hive-column, hive-cell", + "automation/card-chat-night-watch, queen/dispatch, queen/hive" + ] + ] + }, + { + "kind": "p", + "text": "The pictures are the rules in motion: a kanban card that lands in the column a spec fn names, a review gate that opens or stays shut, a state machine whose illegal events grey out, a ship that flies home and fades over 2400 ms, a pool of keys where the next free one gets a ring, a ladder of agents that falls silent at depth 3, a cooldown clock, and comb cells that flash gold, amber, red or grey." + }, + { + "kind": "h", + "text": "Where a stuck pull request lands" + }, + { + "kind": "p", + "text": "review-jam.t27 sorts a pull request that will not move into one of 10 cases, and the earliest rule that matches wins. The figure runs its compiled jam_case on every one of the 512 combinations of its 9 facts. Half of them, 256, are unmeasured, because a PR the Queen could not read is never reported as free; 128 are a draft or fresh PR, and only 2 are no jam at all. jam_is_queens_turn says 318 of the 512 are the Queen's move." + }, + { + "kind": "figure", + "svg": "1. unmeasured2562. draft or fresh1283. stacked on open644. conflicting325. labeled not merged166. refused then pushed47. refused not fixed48. noise red49. no reviewer210. none2of 512 combinations of the 9 facts; gold: the Queen's turn", + "caption": "jam_case of review-jam.t27 over all 512 combinations of its 9 facts, rules in their order of priority, computed by the compiled spec the hive-jam widget runs. Gold bars are cases jam_is_queens_turn gives to the Queen." + }, + { + "kind": "h", + "text": "How the specs got into the browser" + }, + { + "kind": "p", + "text": "No widget copies a rule. scripts/t27-logic.mjs of 999-multibots-telegraf runs t27c gen on a spec, builds the Zig with Zig 0.16 for wasm32 and writes a module with one function per spec fn, and scripts/widget-logic.mjs puts it beside the widget with a byte-identical copy of the spec. On t27 master, 137 of the 139 specs in the three folders ran green and 1302 fns lowered. review-jam found two bugs in that door, fixed in 999#4020: a param named case, which TypeScript refuses, and a bool read with all 32 bits, which made jam_is_queens_turn answer true for none." + }, + { + "kind": "h", + "text": "What it does not show" + }, + { + "kind": "ul", + "items": [ + "No widget reads the live Queen: every input is what the reader sets, and the measured numbers are the ones the specs record, such as 29 dispatches waiting on 2026-10-18.", + "Lesson 26 shows a finding of dispatch.t27 as it stands: once an issue is closed, the public board draws every finished claim as done.", + "The game widgets are flat sketches of the 3D game at app.t27.ai/game, not the game itself." + ] + }, + { + "kind": "h", + "text": "Try it" + }, + { + "kind": "ul", + "items": [ + "Course: t27.ai/learn/queen-and-hive/", + "t27.ai/widgets/hive-jam/, t27.ai/widgets/hive-next-key/, t27.ai/widgets/hive-chain/, t27.ai/widgets/hive-cell/", + "Specs: gHashTag/t27 specs/queen/ and specs/automation/" + ] + } +] + +export const ruBody: Block[] = [ + { + "kind": "p", + "text": "Вышел курс 11 на t27.ai: Королева и улей. Он рассказывает, как этот проект строит себя сам. Задача становится работой для пчелы, пчела пишет спеку .t27, пчела-ревьюер её проверяет, спеку сливают, и сайт пересобирается. Королева, которая ведёт этот цикл, не чёрный ящик: её правила лежат в gHashTag/t27 specs/queen и specs/automation спеками с числовыми тестами. В курсе 27 уроков в 9 модулях, каждый открывается вопросом, и каждый виджет отвечает на него телами fn самой спеки, скомпилированными в WebAssembly." + }, + { + "kind": "h", + "text": "Девять модулей, двадцать семь виджетов" + }, + { + "kind": "table", + "head": [ + "Модуль", + "Виджеты", + "Спеки" + ], + "rows": [ + [ + "Задача", + "hive-shape, hive-boundary, hive-task-filter", + "queen/shaping, queen/tasks" + ], + [ + "Кто следующий", + "hive-aging, hive-rank, hive-review-first", + "queen/priority, queen/review_first" + ], + [ + "Жизнь пчелы", + "hive-lifecycle, hive-ship, hive-drain", + "queen/dispatch, queen/hive, queen/drain" + ], + [ + "Пчела-ревьюер", + "hive-reviewers, hive-revisit, hive-gate", + "queen/reviewer, queen/merger_gate" + ], + [ + "Затор ревью", + "hive-jam, hive-jam-action, hive-master-red", + "automation/review-jam, queen/merger_gate" + ], + [ + "Ключи", + "hive-key-error, hive-key-rest, hive-next-key", + "queen/key_failover" + ], + [ + "Заслуги и деньги", + "hive-credit, hive-price, hive-burn", + "automation/queen-contributor-keys, automation/token-wallet" + ], + [ + "Агенты между собой", + "hive-called, hive-chain, hive-cooldown", + "automation/agent-hive-group" + ], + [ + "Ночь и доска", + "hive-night, hive-column, hive-cell", + "automation/card-chat-night-watch, queen/dispatch, queen/hive" + ] + ] + }, + { + "kind": "p", + "text": "Картинки — это правила в движении: карточка канбана, которая попадает в колонку, названную функцией спеки, ворота ревью, которые открываются или остаются закрытыми, автомат состояний, где недопустимые события сереют, корабль, который летит домой и гаснет за 2400 мс, пул ключей, где следующий свободный получает кольцо, лестница агентов, которая замолкает на глубине 3, часы паузы и ячейки сот, вспыхивающие золотым, янтарным, красным или серым." + }, + { + "kind": "h", + "text": "Куда попадает застрявший пулл-реквест" + }, + { + "kind": "p", + "text": "review-jam.t27 относит пулл-реквест, который не движется, к одному из 10 случаев, и побеждает правило, совпавшее раньше остальных. На рисунке скомпилированная jam_case перебирает все 512 сочетаний её 9 фактов. Половина, 256, — unmeasured, потому что PR, который Королева не смогла прочитать, никогда не считается свободным; 128 — черновик или свежий PR, и всего 2 — вовсе не затор. jam_is_queens_turn говорит, что 318 из 512 — ход Королевы." + }, + { + "kind": "figure", + "svg": "1. unmeasured2562. draft or fresh1283. stacked on open644. conflicting325. labeled not merged166. refused then pushed47. refused not fixed48. noise red49. no reviewer210. none2из 512 сочетаний 9 фактов; золотые — ход Королевы", + "caption": "jam_case из review-jam.t27 на всех 512 сочетаниях её 9 фактов, правила в порядке приоритета; посчитано скомпилированной спекой, которую исполняет виджет hive-jam. Золотые полосы — случаи, которые jam_is_queens_turn отдаёт Королеве." + }, + { + "kind": "h", + "text": "Как спеки попали в браузер" + }, + { + "kind": "p", + "text": "Ни один виджет не копирует правило. scripts/t27-logic.mjs из 999-multibots-telegraf запускает t27c gen на спеке, собирает Zig через Zig 0.16 под wasm32 и пишет модуль с функцией на каждую fn спеки, а scripts/widget-logic.mjs кладёт его рядом с виджетом вместе с побайтной копией спеки. На master t27 из 139 спек трёх папок 137 прошли зелёными, и 1302 fn собрались. review-jam нашла в этой обвязке две ошибки, исправленные в 999#4020: параметр с именем case, который TypeScript не принимает, и bool, прочитанный всеми 32 битами, из-за чего jam_is_queens_turn отвечала true для none." + }, + { + "kind": "h", + "text": "Чего это не показывает" + }, + { + "kind": "ul", + "items": [ + "Ни один виджет не читает живую Королеву: все входы задаёт читатель, а измеренные числа — те, что записаны в спеках, например 29 запусков в ожидании на 2026-10-18.", + "Урок 26 показывает находку dispatch.t27 как есть: когда задача закрыта, публичная доска рисует любое завершённое притязание как готовое.", + "Игровые виджеты — плоские наброски 3D-игры на app.t27.ai/game, а не сама игра." + ] + }, + { + "kind": "h", + "text": "Попробовать" + }, + { + "kind": "ul", + "items": [ + "Курс: t27.ai/learn/queen-and-hive/", + "t27.ai/widgets/hive-jam/, t27.ai/widgets/hive-next-key/, t27.ai/widgets/hive-chain/, t27.ai/widgets/hive-cell/", + "Спеки: gHashTag/t27 specs/queen/ и specs/automation/" + ] + } +] diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index 0665822df9..47bbf3a9c3 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -2,6 +2,36 @@ import type { PostMeta } from './types' /** Индекс блога: список и метаданные без тяжёлых тел публикаций. */ export const postsIndex: PostMeta[] = [ + { + slug: "the-queen-and-the-hive-a-course-on-how-the-project-builds-itself", + title: "The Queen and the hive: a course on how the project builds itself, 27 widgets on compiled specs", + summary: "[every widget runs fn bodies of a gHashTag/t27 specs/queen or specs/automation spec compiled to wasm; no widget reads the live Queen; the game widgets are flat sketches] Course 11 opens the rules the Queen runs the hive by: shaping an issue and its boundary, priority, aging and review first, a task's state machine, the ship in the game, a deploy mid-flight, the reviewer's concurrency and back-off, the merge gate, the review jam and inherited red, refused keys and the next key, credit and token prices, agent bots in one group, the night watch, the public board and the comb.", + date: "2026-10-19", + readingMinutes: 4, + tags: ["t27", "Course", "Queen"], + receipts: [ + { label: "trinity#1698: the course issue, 27 widgets on gHashTag/t27 specs/queen and specs/automation", href: "https://github.com/gHashTag/trinity/issues/1698" }, + { label: "The course: the Queen and the hive", href: "https://t27.ai/learn/queen-and-hive/" }, + { label: "The review jam widget", href: "https://t27.ai/widgets/hive-jam/" }, + { label: "The next key widget", href: "https://t27.ai/widgets/hive-next-key/" }, + { label: "999#4020: two door fixes review-jam needed", href: "https://github.com/gHashTag/999-multibots-telegraf/pull/4020" }, + ], + openQuestions: [ + "No widget reads the live Queen; the measured numbers are the ones the specs record.", + "Lesson 26 shows the board drawing a closed issue's finished claims as done, a finding dispatch.t27 records.", + "The game widgets are flat sketches of the 3D game, not the game.", + ], + published: true, + ru: { + title: "Королева и улей: курс о том, как проект строит себя сам, 27 виджетов на скомпилированных спеках", + summary: "[каждый виджет исполняет тела функций спеки gHashTag/t27 specs/queen или specs/automation, собранные в wasm; ни один виджет не читает живую Королеву; игровые виджеты — плоские наброски] Курс 11 открывает правила, по которым Королева ведёт улей: оформление задачи и её границу, приоритет, старение и сначала ревью, автомат состояний задачи, корабль в игре, деплой на лету, число ревью сразу и паузы ревьюера, ворота слияния, затор ревью и унаследованный красный, отклонённые ключи и следующий ключ, заслуги и цены в токенах, ботов агентов в одной группе, ночной дозор, публичную доску и соты.", + openQuestions: [ + "Ни один виджет не читает живую Королеву; измеренные числа — те, что записаны в спеках.", + "Урок 26 показывает, как доска рисует завершённые притязания закрытой задачи готовыми — эту находку записывает dispatch.t27.", + "Игровые виджеты — плоские наброски 3D-игры, а не сама игра.", + ], + }, + }, { slug: "neural-networks-in-t27-a-widget-per-lesson", title: "Neural networks in t27: 27 lessons, and a widget in each one that runs a specs/ml spec", diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts index 80b3fe53d0..dbd9cb64a3 100644 --- a/apps/website/src/data/blog/posts.ts +++ b/apps/website/src/data/blog/posts.ts @@ -1,3 +1,4 @@ +import { body as body_the_queen_and_the_hive_a_course_on_how_the_project_builds_itself, ruBody as ruBody_the_queen_and_the_hive_a_course_on_how_the_project_builds_itself } from './bodies/the-queen-and-the-hive-a-course-on-how-the-project-builds-itself' import { body as body_a_wasm_interpreter_ran_on_the_board, ruBody as ruBody_a_wasm_interpreter_ran_on_the_board } from './bodies/a-wasm-interpreter-ran-on-the-board' import { body as body_neural_networks_in_t27_a_widget_per_lesson, ruBody as ruBody_neural_networks_in_t27_a_widget_per_lesson } from './bodies/neural-networks-in-t27-a-widget-per-lesson' import { body as body_arithmetic_and_dsp_a_widget_per_lesson, ruBody as ruBody_arithmetic_and_dsp_a_widget_per_lesson } from './bodies/arithmetic-and-dsp-a-widget-per-lesson' @@ -141,6 +142,7 @@ const bodies: Record = { 'bypass-or-a-broken-shift': { body: body_bypass_or_a_broken_shift, ruBody: ruBody_bypass_or_a_broken_shift }, 'the-ternary-machine-course': { body: body_the_ternary_machine_course, ruBody: ruBody_the_ternary_machine_course }, 'three-widgets-under-the-bus': { body: body_three_widgets_under_the_bus, ruBody: ruBody_three_widgets_under_the_bus }, + 'the-queen-and-the-hive-a-course-on-how-the-project-builds-itself': { body: body_the_queen_and_the_hive_a_course_on_how_the_project_builds_itself, ruBody: ruBody_the_queen_and_the_hive_a_course_on_how_the_project_builds_itself }, 'neural-networks-in-t27-a-widget-per-lesson': { body: body_neural_networks_in_t27_a_widget_per_lesson, ruBody: ruBody_neural_networks_in_t27_a_widget_per_lesson }, 'arithmetic-and-dsp-a-widget-per-lesson': { body: body_arithmetic_and_dsp_a_widget_per_lesson, ruBody: ruBody_arithmetic_and_dsp_a_widget_per_lesson }, 'the-back-half-of-openxc7-from-a-spec': { body: body_the_back_half_of_openxc7_from_a_spec, ruBody: ruBody_the_back_half_of_openxc7_from_a_spec }, diff --git a/apps/website/src/lib/course.generated.ts b/apps/website/src/lib/course.generated.ts index 858a1c0ec0..25ed2e96ca 100644 --- a/apps/website/src/lib/course.generated.ts +++ b/apps/website/src/lib/course.generated.ts @@ -1,5 +1,5 @@ -// GENERATED by scripts/course-from-spec.mjs from specs/course/courses.t27, specs/course/t27-basics.t27, specs/course/course.t27, specs/course/ai-numbers.t27, specs/course/goldenfloat.t27, specs/course/verify-hardware.t27, specs/course/clocks-and-cdc.t27, specs/course/buses-and-peripherals.t27, specs/course/ternary-computing.t27, specs/course/arithmetic-and-dsp.t27, specs/course/neural-networks.t27 and their Russian bundles -// spec sha256 t27-basics 4827f6b9f5d95ae98e1d49f037966d2cc826e07c691044f2f5fa1ee9c98ae474, course 8429472829c5d9a0782b2d2b21ff26879a429c5470f4abb5088f1e1a7fa674be, ai-numbers 7f736e357ef13446dfa8f978482cc28c7710caac3f7396769a8fc01b91e20802, goldenfloat d6b6019b8e6bf77a41991b32a3a3d1b5eb78ca4b7078e2bf5f5436df3dd7c59c, verify-hardware 7ca437575eaa8f8646f42c82b75862f091fb509708de152c4e18ed76ee39d4cf, clocks-and-cdc 7d491967f9326fcde194ab19a9bfa7f0ed960ba94d7ca3d82df0747e269c5acc, buses-and-peripherals 4dfdbd8c909d7ad8f1ac2d4f7025c77f0619ade92b181212ea9b589262695acc, ternary-computing e0a14010163f00208d5b54c800e6ce3f05e6b8581de94c0323097b00b1854437, arithmetic-and-dsp c2b6bfd642e88caadaff9da965ac49172b389427b574b27d4c64145851bc206d, neural-networks d6cffe6af3c2bd2ce3f73272eee7cca3e463f8f8e4e034d1dbc39f7f43083bc1 +// GENERATED by scripts/course-from-spec.mjs from specs/course/courses.t27, specs/course/t27-basics.t27, specs/course/course.t27, specs/course/ai-numbers.t27, specs/course/goldenfloat.t27, specs/course/verify-hardware.t27, specs/course/clocks-and-cdc.t27, specs/course/buses-and-peripherals.t27, specs/course/ternary-computing.t27, specs/course/arithmetic-and-dsp.t27, specs/course/neural-networks.t27, specs/course/queen-and-hive.t27 and their Russian bundles +// spec sha256 t27-basics 4827f6b9f5d95ae98e1d49f037966d2cc826e07c691044f2f5fa1ee9c98ae474, course 8429472829c5d9a0782b2d2b21ff26879a429c5470f4abb5088f1e1a7fa674be, ai-numbers 7f736e357ef13446dfa8f978482cc28c7710caac3f7396769a8fc01b91e20802, goldenfloat d6b6019b8e6bf77a41991b32a3a3d1b5eb78ca4b7078e2bf5f5436df3dd7c59c, verify-hardware 7ca437575eaa8f8646f42c82b75862f091fb509708de152c4e18ed76ee39d4cf, clocks-and-cdc 7d491967f9326fcde194ab19a9bfa7f0ed960ba94d7ca3d82df0747e269c5acc, buses-and-peripherals 4dfdbd8c909d7ad8f1ac2d4f7025c77f0619ade92b181212ea9b589262695acc, ternary-computing e0a14010163f00208d5b54c800e6ce3f05e6b8581de94c0323097b00b1854437, arithmetic-and-dsp c2b6bfd642e88caadaff9da965ac49172b389427b574b27d4c64145851bc206d, neural-networks d6cffe6af3c2bd2ce3f73272eee7cca3e463f8f8e4e034d1dbc39f7f43083bc1, queen-and-hive af1f07409e348d465db47725edaf544467db3b8c9fa35ca844281b2174310fde // Do not edit: change the .t27 source (or a Russian bundle) and regenerate. export const COURSES = [ @@ -13620,6 +13620,1649 @@ export const COURSES = [ "ru": "Арифметика и DSP на FPGA: от цепи переноса до фильтра" } }, + "next": { + "id": "queen-and-hive", + "route": "queen-and-hive", + "share": "learn/queen-and-hive/", + "first": "shaping-an-issue", + "last": "the-comb-answers", + "title": { + "en": "The Queen and the hive: how the t27 project builds itself", + "ru": "Королева и улей: как проект t27 строит себя сам" + } + } + }, + { + "id": "queen-and-hive", + "source": { + "spec": "specs/course/queen-and-hive.t27", + "publicSpec": "public/learn/queen-and-hive.t27", + "ruContract": "specs/course/queen-and-hive-ru.t27", + "ruBundle": "apps/website/i18n/queen-and-hive.ru.json", + "sha256": "af1f07409e348d465db47725edaf544467db3b8c9fa35ca844281b2174310fde", + "schemaVersion": 1, + "gallery": "specs/widgets/gallery.t27" + }, + "route": "queen-and-hive", + "share": "learn/queen-and-hive/", + "cohortRoute": "fpga-training", + "progressKey": "t27-course-done", + "sendsNothing": true, + "locales": [ + "en", + "ru" + ], + "say": { + "en": { + "TITLE": "The Queen and the hive: how the t27 project builds itself", + "DESCRIPTION": "Course 11, 27 lessons, a live widget and a t27 spec each: shaping an issue, priority and review first, a task's life, the reviewer and the merge gate, the review jam, keys, credit and tokens, agents in one group, the night watch and the board. Every answer comes from a compiled spec.", + "ALL": "All lessons", + "ALSO": "Also try", + "ALSO_HINT": "Each one swaps the widget above.", + "BACK_TO_MAIN": "Back to this lesson's widget", + "COHORT": "Looking for the paid FPGA training? It moved to its own page.", + "CONTINUE": "Continue", + "GOAL": "You will learn", + "KICKER": "Course", + "LEAD": "This project builds itself: an issue becomes a task for a bee, the bee writes a t27 spec, a reviewer bee checks it, it merges, and the site regenerates. The Queen who runs that loop is written as t27 specs with tests, and every widget here runs the fn bodies of one of them compiled to WebAssembly.", + "LESSON": "Lesson {0} of {1}", + "MARK": "Mark as done", + "MARKED": "Done", + "MODULE": "Module {0}", + "NEXT": "Next", + "NEXT_COURSE": "Next course", + "NOT_FOUND": "No lesson has this address.", + "OPEN_COURSE": "Open the interactive course", + "OPEN_LESSON": "Open the interactive lesson", + "OPEN_PAGE": "Open the widget on its own page", + "PREV": "Previous", + "PREV_COURSE": "Previous course", + "PRIVATE": "Progress stays in this browser; nothing is sent.", + "PROGRESS": "{0} of {1} done", + "SEO_TITLE": "{0}: the Queen and the hive course, lesson {1} of {2}", + "SHAPE": "9 modules of 3 lessons, 27 cells. A filled cell is a lesson you marked done.", + "SHARE": "Link to share, with a preview card", + "SOURCE": "This course is itself a t27 spec: read it", + "SPEC": "Open the lesson's spec in the player", + "START": "Start lesson 1", + "TRY": "Try it", + "WIDGET_LANG": "Widgets keep their own English words: each one lives in its own t27 spec." + }, + "ru": { + "TITLE": "Королева и улей: как проект t27 строит себя сам", + "DESCRIPTION": "Курс 11, 27 уроков, в каждом живой виджет и спека t27: оформление задачи, приоритет и сначала ревью, жизнь задачи, ревьюер и ворота слияния, затор ревью, ключи, заслуги и токены, агенты в одной группе, ночной дозор и доска. Каждый ответ даёт скомпилированная спека.", + "ALL": "Все уроки", + "ALSO": "Также откройте", + "ALSO_HINT": "Каждая кнопка меняет виджет сверху.", + "BACK_TO_MAIN": "Вернуться к виджету урока", + "COHORT": "Ищете платное обучение по FPGA? Оно переехало на отдельную страницу.", + "CONTINUE": "Продолжить", + "GOAL": "Вы узнаете", + "KICKER": "Курс", + "LEAD": "Этот проект строит себя сам: задача становится работой для пчелы, пчела пишет спеку t27, пчела-ревьюер её проверяет, спеку сливают, и сайт пересобирается. Королева, которая ведёт этот цикл, записана спеками t27 с тестами, и каждый виджет здесь исполняет тела fn одной из них, скомпилированные в WebAssembly.", + "LESSON": "Урок {0} из {1}", + "MARK": "Отметить пройденным", + "MARKED": "Пройдено", + "MODULE": "Модуль {0}", + "NEXT": "Далее", + "NEXT_COURSE": "Следующий курс", + "NOT_FOUND": "У урока нет такого адреса.", + "OPEN_COURSE": "Открыть интерактивный курс", + "OPEN_LESSON": "Открыть интерактивный урок", + "OPEN_PAGE": "Открыть виджет на отдельной странице", + "PREV": "Назад", + "PREV_COURSE": "Предыдущий курс", + "PRIVATE": "Прогресс хранится только в этом браузере; ничего не отправляется.", + "PROGRESS": "{0} из {1} пройдено", + "SEO_TITLE": "{0}: курс «Королева и улей», урок {1} из {2}", + "SHAPE": "9 модулей по 3 урока, 27 клеток. Заполненная клетка — урок, отмеченный пройденным.", + "SHARE": "Ссылка для шаринга, с карточкой предпросмотра", + "SOURCE": "Этот курс сам является спекой t27: прочитайте её", + "SPEC": "Открыть спеку урока в плеере", + "START": "Начать урок 1", + "TRY": "Попробовать", + "WIDGET_LANG": "Виджеты хранят свои английские слова: у каждого своя спека t27." + } + }, + "modules": [ + { + "id": "the-task", + "n": 1, + "lessons": [ + "shaping-an-issue", + "a-boundary-of-real-files", + "one-task-three-kinds" + ], + "en": { + "title": "The task", + "line": "An issue becomes work only once it is shaped: the sections it lacks are written or asked for, its boundary names real files, and one filter reads every kind of task." + }, + "ru": { + "title": "Задача", + "line": "Задача становится работой, только когда её оформили: недостающие разделы дописаны или запрошены, граница называет настоящие файлы, а один фильтр читает задачи всех видов." + } + }, + { + "id": "who-goes-next", + "n": 2, + "lessons": [ + "aging-a-label", + "the-queue", + "review-first" + ], + "en": { + "title": "Who goes next", + "line": "The order the Queen takes issues in: a label ages one step, at most 3 criticals run as critical, and nothing new starts while finished work waits for review." + }, + "ru": { + "title": "Кто следующий", + "line": "Порядок, в котором Королева берёт задачи: метка стареет на одну ступень, критичными идут не больше 3, и ничего нового не начинается, пока готовая работа ждёт ревью." + } + }, + { + "id": "a-bees-life", + "n": 3, + "lessons": [ + "a-tasks-life", + "the-ship-flies-home", + "a-deploy-mid-flight" + ], + "en": { + "title": "A bee's life", + "line": "From candidate to done: twelve states and sixteen events, a ship in the game that flies home, and a deploy that does not cut a bee short." + }, + "ru": { + "title": "Жизнь пчелы", + "line": "От кандидата до готовой работы: двенадцать состояний и шестнадцать событий, корабль в игре, который улетает домой, и деплой, который не обрывает пчелу." + } + }, + { + "id": "the-reviewer", + "n": 4, + "lessons": [ + "enough-reviewers", + "back-off", + "the-merge-gate" + ], + "en": { + "title": "The reviewer bee", + "line": "Finished work is judged on its own clock: enough reviews at once to keep up, a pause that doubles for a row that waits, and a merge gate that zero checks cannot open." + }, + "ru": { + "title": "Пчела-ревьюер", + "line": "Готовую работу судят по своим часам: столько ревью сразу, чтобы успевать, пауза, которая удваивается для строки в ожидании, и ворота слияния, которые не открыть без проверок." + } + }, + { + "id": "the-jam", + "n": 5, + "lessons": [ + "the-review-jam", + "the-queens-move", + "inherited-red" + ], + "en": { + "title": "The review jam", + "line": "A pull request that will not move is in exactly one of ten cases, each with the Queen's move, and a red check is inherited only when master says so." + }, + "ru": { + "title": "Затор ревью", + "line": "Застрявший пулл-реквест всегда в одном из десяти случаев, у каждого свой ход Королевы, а красная проверка считается унаследованной, только если так говорит master." + } + }, + { + "id": "keys", + "n": 6, + "lessons": [ + "a-refused-key", + "a-key-rests", + "the-next-key" + ], + "en": { + "title": "Keys", + "line": "A bee's model key can be refused: the code in the body sorts the refusal, a transient one rests the key for seconds, and the bee moves on to the next free key." + }, + "ru": { + "title": "Ключи", + "line": "Ключ модели у пчелы могут отклонить: код в теле ответа сортирует отказ, временный отказ даёт ключу отдохнуть секунды, и пчела переходит к следующему свободному ключу." + } + }, + { + "id": "credit-and-money", + "n": 7, + "lessons": [ + "no-credit-for-keys", + "twice-the-cost", + "tokens-that-burn" + ], + "en": { + "title": "Credit and money", + "line": "Lending a key earns no credit, an operation is sold at twice its cost in half-cent tokens, and tokens burn 30 days after they land." + }, + "ru": { + "title": "Заслуги и деньги", + "line": "Одолженный ключ не приносит заслуг, операция продаётся вдвое дороже своей цены в токенах по полцента, а токены сгорают через 30 дней после зачисления." + } + }, + { + "id": "agents-together", + "n": 8, + "lessons": [ + "who-is-called", + "three-bots-deep", + "stop-and-mute" + ], + "en": { + "title": "Agents among themselves", + "line": "One bot per agent in one group: who is called, a chain three bot messages deep, and the clocks that keep agents from flooding a chat." + }, + "ru": { + "title": "Агенты между собой", + "line": "У каждого агента свой бот в одной группе: кого зовут, цепочка глубиной в три сообщения ботов и часы, которые не дают агентам завалить чат." + } + }, + { + "id": "night-and-the-board", + "n": 9, + "lessons": [ + "the-night-watch", + "the-public-board", + "the-comb-answers" + ], + "en": { + "title": "Night and the board", + "line": "What the hive shows: a night watch that opens, comments on and closes issues, the columns of the public board, and the comb that flashes for each event." + }, + "ru": { + "title": "Ночь и доска", + "line": "Что показывает улей: ночной дозор, который открывает, комментирует и закрывает задачи, колонки публичной доски и соты, которые вспыхивают на каждое событие." + } + } + ], + "lessons": [ + { + "id": "shaping-an-issue", + "n": 1, + "module": "the-task", + "widget": { + "id": "hive-shape", + "kind": "tool", + "title": "Shaping: what the Queen does with an issue that is not ready", + "url": "https://t27.ai/widgets/hive-shape/", + "preview": "widgets/hive-shape/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-boundary", + "kind": "tool", + "title": "A boundary: which files a task may reserve", + "url": "https://t27.ai/widgets/hive-boundary/", + "preview": "widgets/hive-boundary/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-task-filter", + "kind": "tool", + "title": "One task, three kinds: the filter the board and the game share", + "url": "https://t27.ai/widgets/hive-task-filter/", + "preview": "widgets/hive-task-filter/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/shaping.t27", + "source": "t27/files/specs/queen/shaping.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fshaping.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Shaping an issue", + "goal": "What the Queen does with an issue that lacks sections, and why she stops after two tries.", + "text": "A bee can start only on an issue that says what to do and which files it may touch. The Queen holds every open issue to judge() of specs/policy/issue_shape.t27. One that is not ready she shapes: a model writes the missing sections, and the body goes back only if judge() then finds nothing missing. After SHAPE_ATTEMPT_LIMIT, 2 failed shapes, she stops and comments once with what is missing, and an epic is never shaped. shape_action in shaping.t27 decides, and a new try waits 360 minutes.", + "task": "Set two sections missing and step through the tries; then mark the author told and see which column the issue goes to." + }, + "ru": { + "title": "Оформление задачи", + "goal": "Что Королева делает с задачей без нужных разделов и почему останавливается после двух попыток.", + "text": "Пчела может начать только задачу, в которой сказано, что сделать и каких файлов можно касаться. Королева проверяет каждую открытую задачу функцией judge() из specs/policy/issue_shape.t27. Неготовую она оформляет: модель дописывает недостающие разделы, и текст возвращается в задачу, только если judge() больше ничего не находит. После SHAPE_ATTEMPT_LIMIT, 2 неудачных попыток, она останавливается и один раз пишет комментарий, чего не хватает, а эпик не оформляется никогда. Решает shape_action в shaping.t27, а новая попытка ждёт 360 минут.", + "task": "Поставьте два недостающих раздела и пройдите попытки; затем отметьте, что автору уже написали, и посмотрите, в какую колонку попадёт задача." + } + }, + { + "id": "a-boundary-of-real-files", + "n": 2, + "module": "the-task", + "widget": { + "id": "hive-boundary", + "kind": "tool", + "title": "A boundary: which files a task may reserve", + "url": "https://t27.ai/widgets/hive-boundary/", + "preview": "widgets/hive-boundary/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-shape", + "kind": "tool", + "title": "Shaping: what the Queen does with an issue that is not ready", + "url": "https://t27.ai/widgets/hive-shape/", + "preview": "widgets/hive-shape/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-task-filter", + "kind": "tool", + "title": "One task, three kinds: the filter the board and the game share", + "url": "https://t27.ai/widgets/hive-task-filter/", + "preview": "widgets/hive-task-filter/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/shaping.t27", + "source": "t27/files/specs/queen/shaping.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fshaping.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "A boundary of real files", + "goal": "Which proposed paths a task may reserve, and why one invented path stops the whole shape.", + "text": "A task's boundary is a reservation: while a bee works, no other bee takes those files. So a wrong boundary is worse than a missing one, because it holds files the work does not own. path_accepted takes any file that exists, and a new file only when its folder exists, the issue cites it, and its language is one the only-t27 rule allows. shape_lands writes the shape back only when no section is missing, it names 1 to 12 paths, and none of them was refused.", + "task": "Make the two red paths green by changing only facts that could be true; then leave a section missing and watch the gate." + }, + "ru": { + "title": "Граница из настоящих файлов", + "goal": "Какие предложенные пути задача может забронировать и почему один выдуманный путь останавливает всё оформление.", + "text": "Граница задачи — это бронь: пока пчела работает, другие пчёлы эти файлы не берут. Поэтому неверная граница хуже отсутствующей: она держит файлы, которые работе не принадлежат. path_accepted принимает любой существующий файл, а новый — только если существует его папка, задача его упоминает и язык разрешён правилом only-t27. shape_lands возвращает оформление в задачу, только если все разделы на месте, путей от 1 до 12 и ни один не отклонён.", + "task": "Сделайте два красных пути зелёными, меняя только то, что может быть правдой; затем оставьте раздел недописанным и посмотрите на ворота." + } + }, + { + "id": "one-task-three-kinds", + "n": 3, + "module": "the-task", + "widget": { + "id": "hive-task-filter", + "kind": "tool", + "title": "One task, three kinds: the filter the board and the game share", + "url": "https://t27.ai/widgets/hive-task-filter/", + "preview": "widgets/hive-task-filter/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-shape", + "kind": "tool", + "title": "Shaping: what the Queen does with an issue that is not ready", + "url": "https://t27.ai/widgets/hive-shape/", + "preview": "widgets/hive-shape/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-column", + "kind": "tool", + "title": "The public board: which column a task's card is in", + "url": "https://t27.ai/widgets/hive-column/", + "preview": "widgets/hive-column/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-lifecycle", + "kind": "tool", + "title": "A task's life: the Queen's state machine", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/tasks.t27", + "source": "t27/files/specs/queen/tasks.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Ftasks.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "One task, three kinds", + "goal": "How an issue, a review and a job become one kind of task with seven states, and how one filter reads them.", + "text": "The Queen runs three kinds of work: issues for bees, reviews of finished work, and jobs. Each kept its own codes, so no board could show them together. tasks.t27 gives them 7 shared states, numbered as the board's columns plus failed. A cancelled job shows as dropped, and so does a review row that was forgotten. task_matches is the filter the board, the game and the route all apply: a bit per kind, a bit per state, and 0 means any.", + "task": "Show only failed tasks of every kind; then find which job state shows as dropped." + }, + "ru": { + "title": "Одна задача, три вида", + "goal": "Как задача, ревью и фоновая работа становятся одним видом задачи с семью состояниями и как их читает один фильтр.", + "text": "Королева ведёт работу трёх видов: задачи для пчёл, ревью готовой работы и фоновые работы. У каждого вида были свои коды, и ни одна доска не могла показать их вместе. tasks.t27 даёт им 7 общих состояний, пронумерованных как колонки доски, плюс failed. Отменённая работа показывается как dropped, и так же — забытая строка ревью. task_matches — фильтр, который применяют доска, игра и маршрут: бит на вид, бит на состояние, а 0 значит любое.", + "task": "Покажите только упавшие задачи всех видов; затем найдите, какое состояние работы показывается как dropped." + } + }, + { + "id": "aging-a-label", + "n": 4, + "module": "who-goes-next", + "widget": { + "id": "hive-aging", + "kind": "tool", + "title": "Aging: how long a labelled low issue waits", + "url": "https://t27.ai/widgets/hive-aging/", + "preview": "widgets/hive-aging/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-rank", + "kind": "tool", + "title": "The queue: which issue the Queen takes first", + "url": "https://t27.ai/widgets/hive-rank/", + "preview": "widgets/hive-rank/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-review-first", + "kind": "tool", + "title": "Review first: no new work while finished work waits", + "url": "https://t27.ai/widgets/hive-review-first/", + "preview": "widgets/hive-review-first/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/priority.t27", + "source": "t27/files/specs/queen/priority.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fpriority.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Aging a label", + "goal": "How a labelled issue moves up with age, and why only by one level.", + "text": "Without labels the Queen takes issues in GitHub's listing order. priority.t27 adds four levels, critical, high, normal and low; lower runs first, and an unlabelled issue counts as normal. A labelled issue gains one level after AGING_DAYS, 14 days, and at most MAX_AGING_STEPS, 1 level, so a low issue reaches normal on day 14 and stays there. High and critical do not age. effective_level computes the line the widget draws.", + "task": "Find the day a labelled low issue moves up; then remove the label and drag to day 60." + }, + "ru": { + "title": "Старение метки", + "goal": "Как задача с меткой поднимается с возрастом и почему только на один уровень.", + "text": "Без меток Королева берёт задачи в том порядке, в котором их выдаёт GitHub. priority.t27 добавляет четыре уровня: critical, high, normal и low; меньший идёт раньше, а задача без метки считается normal. Задача с меткой поднимается на уровень через AGING_DAYS, 14 дней, и не больше чем на MAX_AGING_STEPS, 1 уровень, так что low доходит до normal на 14-й день и там остаётся. High и critical не стареют. Линию в виджете вычисляет effective_level.", + "task": "Найдите день, когда задача с меткой low поднимается; затем снимите метку и дотяните до 60-го дня." + } + }, + { + "id": "the-queue", + "n": 5, + "module": "who-goes-next", + "widget": { + "id": "hive-rank", + "kind": "tool", + "title": "The queue: which issue the Queen takes first", + "url": "https://t27.ai/widgets/hive-rank/", + "preview": "widgets/hive-rank/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-aging", + "kind": "tool", + "title": "Aging: how long a labelled low issue waits", + "url": "https://t27.ai/widgets/hive-aging/", + "preview": "widgets/hive-aging/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-review-first", + "kind": "tool", + "title": "Review first: no new work while finished work waits", + "url": "https://t27.ai/widgets/hive-review-first/", + "preview": "widgets/hive-review-first/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/priority.t27", + "source": "t27/files/specs/queen/priority.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fpriority.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The queue", + "goal": "How the cap, aging and blockers order a queue of open issues.", + "text": "Labels inflate: if every issue is critical, none is. capped_level lets at most CRITICAL_CAP, 3, issues of a listing run as critical, taken in listing order, and the 4th runs as high. An issue with an open blocker is skipped, not waited on, so it cannot hold up the queue. outranks compares the effective level, then the real level, then the place in the listing, so an aged issue never beats a real one of its level. why names the reason for each place.", + "task": "Make a fifth issue critical and see where it lands; then label the unlabelled issue low." + }, + "ru": { + "title": "Очередь", + "goal": "Как потолок, старение и блокеры упорядочивают очередь открытых задач.", + "text": "Метки раздуваются: если каждая задача критична, критичных нет. capped_level позволяет идти как critical не больше чем CRITICAL_CAP, 3, задачам списка, в порядке списка, а 4-я идёт как high. Задача с открытым блокером пропускается, её не ждут, и она не может задержать очередь. outranks сравнивает уровень с учётом старения, затем настоящий уровень, затем место в списке, так что постаревшая задача никогда не обгонит настоящую своего уровня. why называет причину каждого места.", + "task": "Сделайте критичной пятую задачу и посмотрите, куда она встанет; затем поставьте задаче без метки метку low." + } + }, + { + "id": "review-first", + "n": 6, + "module": "who-goes-next", + "widget": { + "id": "hive-review-first", + "kind": "tool", + "title": "Review first: no new work while finished work waits", + "url": "https://t27.ai/widgets/hive-review-first/", + "preview": "widgets/hive-review-first/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-rank", + "kind": "tool", + "title": "The queue: which issue the Queen takes first", + "url": "https://t27.ai/widgets/hive-rank/", + "preview": "widgets/hive-rank/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-reviewers", + "kind": "tool", + "title": "How many reviewer bees: keeping up with the finish rate", + "url": "https://t27.ai/widgets/hive-reviewers/", + "preview": "widgets/hive-reviewers/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/review_first.t27", + "source": "t27/files/specs/queen/review_first.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Freview_first.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Review first", + "goal": "Why the Queen closes the backlog while finished work waits for a review, and what still goes.", + "text": "On 2026-10-18 the status page read 69 of 70 workers busy, and in one hour 102 dispatches, 56 finished and 11 reviewed. Bees started about nine for every verdict, and each new bee held files that the bees in review needed when they were sent back. review_first.t27 closes the backlog while the review queue is longer than REVIEW_QUEUE_MAX, 0. Work the review sent back and work the owner assigned still go: admission admits them at any queue length.", + "task": "Drag the queue to 0 and back to 1; then find the candidates that are admitted at any length." + }, + "ru": { + "title": "Сначала ревью", + "goal": "Почему Королева закрывает бэклог, пока готовая работа ждёт ревью, и что всё равно идёт.", + "text": "На 2026-10-18 страница статуса показывала 69 из 70 занятых воркеров, а за час — 102 запуска, 56 завершённых и 11 проверенных. Пчёл запускалось примерно девять на каждый вердикт, и каждая новая пчела держала файлы, нужные пчёлам на ревью, когда их возвращали. review_first.t27 закрывает бэклог, пока очередь ревью длиннее REVIEW_QUEUE_MAX, 0. Работа, которую ревью вернуло, и работа, назначенная владельцем, всё равно идут: admission пропускает их при любой длине очереди.", + "task": "Перетащите очередь на 0 и обратно на 1; затем найдите кандидатов, которых пропускают при любой длине." + } + }, + { + "id": "a-tasks-life", + "n": 7, + "module": "a-bees-life", + "widget": { + "id": "hive-lifecycle", + "kind": "tool", + "title": "A task's life: the Queen's state machine", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-ship", + "kind": "tool", + "title": "The game: where a bee flies and how it leaves", + "url": "https://t27.ai/widgets/hive-ship/", + "preview": "widgets/hive-ship/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-drain", + "kind": "tool", + "title": "A deploy mid-flight: what happens to a running bee", + "url": "https://t27.ai/widgets/hive-drain/", + "preview": "widgets/hive-drain/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-column", + "kind": "tool", + "title": "The public board: which column a task's card is in", + "url": "https://t27.ai/widgets/hive-column/", + "preview": "widgets/hive-column/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/dispatch.t27", + "source": "t27/files/specs/queen/dispatch.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fdispatch.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "A task's life", + "goal": "Which states a dispatched task passes through, and which events are legal in each.", + "text": "dispatch.t27 is the Queen's task lifecycle, the cycle that chooses, starts, ends, reviews and releases work. A task has 12 states and 16 events. next_state answers every pair, and 99 means the event is illegal in that state: a verdict cannot reach a running task, and a reap cannot reach an ended one. After a verdict a send-back waits for its floor and is released; even an accept is sent back if CI turns red under the ceiling.", + "task": "Walk a task from none to accept; then make CI red at the ceiling and find the one way out." + }, + "ru": { + "title": "Жизнь задачи", + "goal": "Через какие состояния проходит запущенная задача и какие события допустимы в каждом.", + "text": "dispatch.t27 — жизненный цикл задачи у Королевы, цикл, который выбирает, запускает, завершает, проверяет и отпускает работу. У задачи 12 состояний и 16 событий. next_state отвечает на каждую пару, а 99 значит, что событие в этом состоянии недопустимо: вердикт не доходит до работающей задачи, а снятие — до завершённой. После вердикта возврат ждёт свой порог и отпускается; даже принятую работу возвращают, если CI краснеет ниже потолка.", + "task": "Проведите задачу от none до accept; затем сделайте CI красным на потолке и найдите выход, который остаётся." + } + }, + { + "id": "the-ship-flies-home", + "n": 8, + "module": "a-bees-life", + "widget": { + "id": "hive-ship", + "kind": "tool", + "title": "The game: where a bee flies and how it leaves", + "url": "https://t27.ai/widgets/hive-ship/", + "preview": "widgets/hive-ship/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-lifecycle", + "kind": "tool", + "title": "A task's life: the Queen's state machine", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-cell", + "kind": "tool", + "title": "The comb answers: what an event does to a cell", + "url": "https://t27.ai/widgets/hive-cell/", + "preview": "widgets/hive-cell/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/hive.t27", + "source": "t27/files/specs/queen/hive.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fhive.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The ship flies home", + "goal": "Where the game draws a bee for each thing it is doing, and how a finished task ends on screen.", + "text": "In the game at app.t27.ai/game each bee is a ship over the issue it works on. ship_mode in hive.t27 picks the place: parked at the entrance before a runner claims it, orbiting its cell while it is heard from, hovering when it has gone quiet, and waiting at its repository's portal until its cell is placed. When the bee leaves the feed it flies home, and ship_alpha fades it over DEPART_MS, 2400 ms: at 1200 ms it is at 500 of 1000.", + "task": "Mark the bee as not heard from and see where it moves; then drag the time past 2400 ms." + }, + "ru": { + "title": "Корабль летит домой", + "goal": "Где игра рисует пчелу в каждом её занятии и как завершённая задача заканчивается на экране.", + "text": "В игре на app.t27.ai/game каждая пчела — корабль над задачей, над которой она работает. ship_mode в hive.t27 выбирает место: стоянка у входа, пока раннер её не взял, облёт своей ячейки, пока от неё приходят вести, зависание, когда она замолчала, и ожидание у портала своего репозитория, пока ячейку не поставили. Когда пчела уходит из ленты, она летит домой, и ship_alpha гасит её за DEPART_MS, 2400 мс: через 1200 мс у неё 500 из 1000.", + "task": "Отметьте, что от пчелы давно нет вестей, и посмотрите, куда она сдвинется; затем протяните время дальше 2400 мс." + } + }, + { + "id": "a-deploy-mid-flight", + "n": 9, + "module": "a-bees-life", + "widget": { + "id": "hive-drain", + "kind": "tool", + "title": "A deploy mid-flight: what happens to a running bee", + "url": "https://t27.ai/widgets/hive-drain/", + "preview": "widgets/hive-drain/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-lifecycle", + "kind": "tool", + "title": "A task's life: the Queen's state machine", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-ship", + "kind": "tool", + "title": "The game: where a bee flies and how it leaves", + "url": "https://t27.ai/widgets/hive-ship/", + "preview": "widgets/hive-ship/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/drain.t27", + "source": "t27/files/specs/queen/drain.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fdrain.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "A deploy mid-flight", + "goal": "What a redeploy does to a bee, a review, a round, a job and a wait.", + "text": "Every merge redeploys the Queen, and work is running when it does. drain.t27 sorts that work. A round, a job step and a wait row are rows the store keeps: the process leaves them and the next process carries on. A review is stopped, because a stop loses at most one review, which is redone. A bee is held, because what it has done lives in its conversation; at the cap, drain_cap_seconds, 1800 s today, it is handed back and its work is salvaged into its branch.", + "task": "Drag the wait from 0 to the cap and watch the bee's row change; then cut the platform drain to 1000 s." + }, + "ru": { + "title": "Деплой на лету", + "goal": "Что передеплой делает с пчелой, ревью, раундом, фоновой работой и ожиданием.", + "text": "Каждое слияние передеплоивает Королеву, а в этот момент идёт работа. drain.t27 её сортирует. Раунд, шаг фоновой работы и строка ожидания — это строки, которые хранит база: процесс их оставляет, и следующий продолжает. Ревью останавливается: остановка теряет не больше одного ревью, и его делают заново. Пчелу держат, потому что сделанное ею живёт в её разговоре; на потолке, drain_cap_seconds, сегодня 1800 с, её возвращают, а работу сохраняют в её ветку.", + "task": "Протяните ожидание от 0 до потолка и посмотрите, как меняется строка пчелы; затем сократите дренаж платформы до 1000 с." + } + }, + { + "id": "enough-reviewers", + "n": 10, + "module": "the-reviewer", + "widget": { + "id": "hive-reviewers", + "kind": "tool", + "title": "How many reviewer bees: keeping up with the finish rate", + "url": "https://t27.ai/widgets/hive-reviewers/", + "preview": "widgets/hive-reviewers/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-revisit", + "kind": "tool", + "title": "Back off: when the reviewer looks at a waiting row again", + "url": "https://t27.ai/widgets/hive-revisit/", + "preview": "widgets/hive-revisit/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-gate", + "kind": "tool", + "title": "The merge gate: when an approved pull request may merge", + "url": "https://t27.ai/widgets/hive-gate/", + "preview": "widgets/hive-gate/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-review-first", + "kind": "tool", + "title": "Review first: no new work while finished work waits", + "url": "https://t27.ai/widgets/hive-review-first/", + "preview": "widgets/hive-review-first/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/reviewer.t27", + "source": "t27/files/specs/queen/reviewer.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Freviewer.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Enough reviewers", + "goal": "How many reviews must run at once for the reviewer to keep up with the bees.", + "text": "A review may take REVIEW_ROW_SECONDS, 300 s, so one slot clears at most 12 rows an hour. On 2026-10-09 bees finished about 58 an hour while 111 rows waited, and the fixed REVIEWER_CONCURRENCY of 4 tops out at 48. slots_needed(58) is 5. reviewer_concurrency takes the keys, their lanes and the free memory, never goes above 16 and never below 1 while anything waits, and keeps_up compares it with the finish rate.", + "task": "Find the fewest keys of 1 lane that keep up with 58 an hour; then cut the memory to 1000 MB." + }, + "ru": { + "title": "Хватит ли ревьюеров", + "goal": "Сколько ревью должно идти сразу, чтобы ревьюер успевал за пчёлами.", + "text": "Ревью может занять REVIEW_ROW_SECONDS, 300 с, поэтому один слот проверяет не больше 12 строк в час. На 2026-10-09 пчёлы заканчивали около 58 в час, пока ждали 111 строк, а постоянный REVIEWER_CONCURRENCY, равный 4, даёт не больше 48. slots_needed(58) равно 5. reviewer_concurrency учитывает ключи, их полосы и свободную память, никогда не поднимается выше 16 и не опускается ниже 1, пока что-то ждёт, а keeps_up сравнивает это с темпом завершения.", + "task": "Найдите наименьшее число ключей с 1 полосой, при котором ревьюер успевает за 58 в час; затем урежьте память до 1000 МБ." + } + }, + { + "id": "back-off", + "n": 11, + "module": "the-reviewer", + "widget": { + "id": "hive-revisit", + "kind": "tool", + "title": "Back off: when the reviewer looks at a waiting row again", + "url": "https://t27.ai/widgets/hive-revisit/", + "preview": "widgets/hive-revisit/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-reviewers", + "kind": "tool", + "title": "How many reviewer bees: keeping up with the finish rate", + "url": "https://t27.ai/widgets/hive-reviewers/", + "preview": "widgets/hive-reviewers/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-key-rest", + "kind": "tool", + "title": "How long a refused key rests", + "url": "https://t27.ai/widgets/hive-key-rest/", + "preview": "widgets/hive-key-rest/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/reviewer.t27", + "source": "t27/files/specs/queen/reviewer.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Freviewer.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Back off", + "goal": "Why a row that answers wait is visited less and less often, and how often at least.", + "text": "Over 44 minutes on 2026-10-09 the reviewer made 941 row visits, 888 of them wait, from 22 rows whose branch it could not read; one row was visited 54 times. revisit_after_seconds now waits 30 s after one wait and doubles per wait, never past 900 s: 30, 60, 120, 240, 480, 900. due_again lets the row be taken only once that time has passed, and any other answer resets the count.", + "task": "Find the wait count where the pause stops growing; then set 4 waits and find the second the row is due." + }, + "ru": { + "title": "Отступить", + "goal": "Почему строку, которая отвечает wait, проверяют всё реже и как часто хотя бы.", + "text": "За 44 минуты на 2026-10-09 ревьюер сделал 941 визит к строкам, из них 888 с ответом wait, от 22 строк, чью ветку он не мог прочитать; одну строку проверили 54 раза. Теперь revisit_after_seconds ждёт 30 с после одного wait и удваивает паузу на каждый следующий, не больше 900 с: 30, 60, 120, 240, 480, 900. due_again даёт взять строку, только когда это время прошло, а любой другой ответ сбрасывает счёт.", + "task": "Найдите число ожиданий, после которого пауза перестаёт расти; затем поставьте 4 ожидания и найдите секунду, когда строка снова к сроку." + } + }, + { + "id": "the-merge-gate", + "n": 12, + "module": "the-reviewer", + "widget": { + "id": "hive-gate", + "kind": "tool", + "title": "The merge gate: when an approved pull request may merge", + "url": "https://t27.ai/widgets/hive-gate/", + "preview": "widgets/hive-gate/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-master-red", + "kind": "tool", + "title": "Inherited red: what master's runs say about a red check", + "url": "https://t27.ai/widgets/hive-master-red/", + "preview": "widgets/hive-master-red/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-reviewers", + "kind": "tool", + "title": "How many reviewer bees: keeping up with the finish rate", + "url": "https://t27.ai/widgets/hive-reviewers/", + "preview": "widgets/hive-reviewers/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/merger_gate.t27", + "source": "t27/files/specs/queen/merger_gate.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fmerger_gate.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The merge gate", + "goal": "When the merger may merge a pull request a reviewer bee approved.", + "text": "The reviewer bee approves, and the merger merges; merger_gate.t27 is the rule between them. check_passes lets a green check through and blocks any check still running. A red one passes only when it is not required, has concluded, and the bee named it as discounted, and with the ruleset unreadable every check counts as required. gate_open needs at least 1 posted check, 0 running and 0 blocking: zero checks is not zero failures.", + "task": "Let the running check conclude green; then make the ruleset unreadable and watch the discounted red." + }, + "ru": { + "title": "Ворота слияния", + "goal": "Когда мерджер может слить пулл-реквест, который одобрила пчела-ревьюер.", + "text": "Пчела-ревьюер одобряет, мерджер сливает; merger_gate.t27 — правило между ними. check_passes пропускает зелёную проверку и задерживает любую, которая ещё идёт. Красная проходит, только если она не обязательная, завершилась и пчела назвала её списанной, а если набор правил не читается, обязательной считается каждая проверка. gate_open нужен хотя бы 1 опубликованный результат, 0 идущих и 0 блокирующих: ноль проверок — не ноль падений.", + "task": "Дайте идущей проверке завершиться зелёной; затем сделайте набор правил нечитаемым и посмотрите на списанную красную." + } + }, + { + "id": "the-review-jam", + "n": 13, + "module": "the-jam", + "widget": { + "id": "hive-jam", + "kind": "tool", + "title": "The review jam: which case a stuck pull request is in", + "url": "https://t27.ai/widgets/hive-jam/", + "preview": "widgets/hive-jam/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-jam-action", + "kind": "tool", + "title": "The Queen's move: what each jam asks of her", + "url": "https://t27.ai/widgets/hive-jam-action/", + "preview": "widgets/hive-jam-action/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-master-red", + "kind": "tool", + "title": "Inherited red: what master's runs say about a red check", + "url": "https://t27.ai/widgets/hive-master-red/", + "preview": "widgets/hive-master-red/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/review-jam.t27", + "source": "t27/files/specs/automation/review-jam.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Freview-jam.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The review jam", + "goal": "Which of ten cases a stuck pull request is in, and why the earliest matching rule wins.", + "text": "A pull request can stall for many reasons at once: refused, pushed again, and red on a noise check. review-jam.t27 gives the Queen one answer. jam_case checks 9 facts in a fixed order and the earliest rule that matches wins, so a PR the Queen could not measure is never reported as free, and one that was refused and then pushed is not taken for noise. There are 10 cases, from none to unmeasured.", + "task": "Clear facts one by one from the start state and name each case you pass; then find a PR that is in no jam." + }, + "ru": { + "title": "Затор ревью", + "goal": "В каком из десяти случаев застрявший пулл-реквест и почему побеждает правило, совпавшее раньше остальных.", + "text": "Пулл-реквест может застрять сразу по нескольким причинам: отклонён, снова запушен и красный на шумовой проверке. review-jam.t27 даёт Королеве один ответ. jam_case проверяет 9 фактов в постоянном порядке, и побеждает правило, совпавшее раньше остальных, так что PR, который Королева не смогла измерить, никогда не считается свободным, а отклонённый и затем запушенный не принимается за шум. Случаев 10, от none до unmeasured.", + "task": "Снимайте факты по одному со стартового состояния и называйте каждый случай; затем найдите PR, который ни в каком заторе." + } + }, + { + "id": "the-queens-move", + "n": 14, + "module": "the-jam", + "widget": { + "id": "hive-jam-action", + "kind": "tool", + "title": "The Queen's move: what each jam asks of her", + "url": "https://t27.ai/widgets/hive-jam-action/", + "preview": "widgets/hive-jam-action/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-jam", + "kind": "tool", + "title": "The review jam: which case a stuck pull request is in", + "url": "https://t27.ai/widgets/hive-jam/", + "preview": "widgets/hive-jam/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-gate", + "kind": "tool", + "title": "The merge gate: when an approved pull request may merge", + "url": "https://t27.ai/widgets/hive-gate/", + "preview": "widgets/hive-gate/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/review-jam.t27", + "source": "t27/files/specs/automation/review-jam.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Freview-jam.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The Queen's move", + "goal": "What the Queen does for each jam, and which jams she waits on.", + "text": "Each case has one action at the same position: no reviewer dispatches a reviewer, refused not fixed dispatches the coder, refused then pushed dispatches a re-review, and labeled not merged finishes the merge. jam_action maps any value above 9 to measure again, so an unknown case is never nothing to do. jam_is_queens_turn is false for 3 cases, none, draft or fresh, and stacked on an open PR: she waits on those.", + "task": "Find the case whose action is wait for the base; then drag to 200 and read the action." + }, + "ru": { + "title": "Ход Королевы", + "goal": "Что Королева делает в каждом заторе и каких заторов она ждёт.", + "text": "У каждого случая одно действие на той же позиции: no reviewer отправляет ревьюера, refused not fixed отправляет кодера, refused then pushed отправляет повторное ревью, а labeled not merged завершает слияние. jam_action переводит любое значение больше 9 в measure again, так что неизвестный случай никогда не значит «делать нечего». jam_is_queens_turn ложно для 3 случаев — none, draft or fresh и stacked on an open PR: их она ждёт.", + "task": "Найдите случай, чьё действие — ждать базу; затем дотяните до 200 и прочитайте действие." + } + }, + { + "id": "inherited-red", + "n": 15, + "module": "the-jam", + "widget": { + "id": "hive-master-red", + "kind": "tool", + "title": "Inherited red: what master's runs say about a red check", + "url": "https://t27.ai/widgets/hive-master-red/", + "preview": "widgets/hive-master-red/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-gate", + "kind": "tool", + "title": "The merge gate: when an approved pull request may merge", + "url": "https://t27.ai/widgets/hive-gate/", + "preview": "widgets/hive-gate/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-jam", + "kind": "tool", + "title": "The review jam: which case a stuck pull request is in", + "url": "https://t27.ai/widgets/hive-jam/", + "preview": "widgets/hive-jam/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/merger_gate.t27", + "source": "t27/files/specs/queen/merger_gate.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fmerger_gate.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Inherited red", + "goal": "When a red check on a pull request is inherited from master, and why a cancelled run says nothing.", + "text": "A bee may approve a PR with a red check when that check is red on master too. On 2026-10-06 a PR merged red because master's run of that check was cancelled and was read as red. Now the merger reads master itself, newest run first: run_says counts only a completed failure or success, look_back passes over everything else and over runs older than the merge base, and discount_holds needs the bee's word and MASTER RED.", + "task": "Make every run cancelled; then move the failure before the merge base and look at the gate." + }, + "ru": { + "title": "Унаследованный красный", + "goal": "Когда красная проверка пулл-реквеста унаследована от master и почему отменённый запуск ничего не говорит.", + "text": "Пчела может одобрить PR с красной проверкой, если эта проверка красная и в master. На 2026-10-06 PR слили красным, потому что запуск этой проверки в master был отменён и прочитан как красный. Теперь мерджер сам читает master, начиная с самого нового запуска: run_says учитывает только завершённое падение или успех, look_back пропускает всё остальное и запуски старше базы слияния, а discount_holds нужны слово пчелы и MASTER RED.", + "task": "Сделайте все запуски отменёнными; затем передвиньте падение раньше базы слияния и посмотрите на ворота." + } + }, + { + "id": "a-refused-key", + "n": 16, + "module": "keys", + "widget": { + "id": "hive-key-error", + "kind": "tool", + "title": "A refused key: transient, quota, auth or a bad request", + "url": "https://t27.ai/widgets/hive-key-error/", + "preview": "widgets/hive-key-error/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-key-rest", + "kind": "tool", + "title": "How long a refused key rests", + "url": "https://t27.ai/widgets/hive-key-rest/", + "preview": "widgets/hive-key-rest/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-next-key", + "kind": "tool", + "title": "The next key: a bee moves on instead of dying", + "url": "https://t27.ai/widgets/hive-next-key/", + "preview": "widgets/hive-next-key/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/key_failover.t27", + "source": "t27/files/specs/queen/key_failover.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fkey_failover.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "A refused key", + "goal": "How the Queen sorts a provider's refusal into transient, quota, auth or a bad request.", + "text": "Bees call models with keys, and keys get refused. key_failover.t27 sorts each refusal with error_kind. The z.ai code in the body wins over the HTTP status, because z.ai sends a spent quota and a passing overload with the same 429: code 1305 is transient, 1113 is quota. No answer at all is transient. A quota or auth refusal closes the key, and only a bad request is charged to the issue.", + "task": "Compare 429 with codes 1308 and 1312; then read what no answer at all is." + }, + "ru": { + "title": "Отклонённый ключ", + "goal": "Как Королева сортирует отказ провайдера: временный, квота, доступ или неверный запрос.", + "text": "Пчёлы обращаются к моделям по ключам, и ключи отклоняют. key_failover.t27 сортирует каждый отказ функцией error_kind. Код z.ai в теле ответа важнее статуса HTTP, потому что z.ai отвечает одним и тем же 429 и на исчерпанную квоту, и на временную перегрузку: код 1305 временный, 1113 — квота. Отсутствие ответа — тоже временный отказ. Отказ по квоте или доступу закрывает ключ, а в счёт задачи идёт только неверный запрос.", + "task": "Сравните 429 с кодами 1308 и 1312; затем прочитайте, что значит отсутствие ответа." + } + }, + { + "id": "a-key-rests", + "n": 17, + "module": "keys", + "widget": { + "id": "hive-key-rest", + "kind": "tool", + "title": "How long a refused key rests", + "url": "https://t27.ai/widgets/hive-key-rest/", + "preview": "widgets/hive-key-rest/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-key-error", + "kind": "tool", + "title": "A refused key: transient, quota, auth or a bad request", + "url": "https://t27.ai/widgets/hive-key-error/", + "preview": "widgets/hive-key-error/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-next-key", + "kind": "tool", + "title": "The next key: a bee moves on instead of dying", + "url": "https://t27.ai/widgets/hive-next-key/", + "preview": "widgets/hive-next-key/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-revisit", + "kind": "tool", + "title": "Back off: when the reviewer looks at a waiting row again", + "url": "https://t27.ai/widgets/hive-revisit/", + "preview": "widgets/hive-revisit/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/key_failover.t27", + "source": "t27/files/specs/queen/key_failover.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fkey_failover.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "A key rests", + "goal": "How long a refused key rests before a bee may use it again.", + "text": "A transient refusal rests the key COOL_BASE_SECONDS, 15 s, doubled per strike in a row and never more than COOL_CAP_SECONDS, 240 s: 15, 30, 60, 120, 240. A spent quota or a refused key closes it for KEY_BREAK_SECONDS, 1800 s. cooldown_seconds gives all of it. The rest is short so that 34 keys are never all resting through a one-minute burst like the one of 2026-10-17.", + "task": "Find the strike count where the rest stops growing; then switch to quota and read the rest." + }, + "ru": { + "title": "Ключ отдыхает", + "goal": "Сколько отклонённый ключ отдыхает, прежде чем пчела сможет взять его снова.", + "text": "Временный отказ даёт ключу отдохнуть COOL_BASE_SECONDS, 15 с, вдвое больше за каждый отказ подряд и не больше COOL_CAP_SECONDS, 240 с: 15, 30, 60, 120, 240. Исчерпанная квота или отклонённый ключ закрывают его на KEY_BREAK_SECONDS, 1800 с. Всё это даёт cooldown_seconds. Отдых короткий, чтобы 34 ключа никогда не отдыхали все сразу во время минутного всплеска, как на 2026-10-17.", + "task": "Найдите число отказов, после которого отдых перестаёт расти; затем переключите на квоту и прочитайте отдых." + } + }, + { + "id": "the-next-key", + "n": 18, + "module": "keys", + "widget": { + "id": "hive-next-key", + "kind": "tool", + "title": "The next key: a bee moves on instead of dying", + "url": "https://t27.ai/widgets/hive-next-key/", + "preview": "widgets/hive-next-key/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-key-error", + "kind": "tool", + "title": "A refused key: transient, quota, auth or a bad request", + "url": "https://t27.ai/widgets/hive-key-error/", + "preview": "widgets/hive-key-error/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-key-rest", + "kind": "tool", + "title": "How long a refused key rests", + "url": "https://t27.ai/widgets/hive-key-rest/", + "preview": "widgets/hive-key-rest/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-credit", + "kind": "tool", + "title": "Lending a key earns no credit: XP follows recorded work", + "url": "https://t27.ai/widgets/hive-credit/", + "preview": "widgets/hive-credit/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/key_failover.t27", + "source": "t27/files/specs/queen/key_failover.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fkey_failover.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The next key", + "goal": "Which key a bee tries next, and when it moves to another pool or gives up.", + "text": "On 2026-10-17 a bee started on key 9 of 10 and died 7 s later while 33 other keys stood by: it got one key at dispatch and kept it. next_free now picks the next key after the last one that is neither tried nor resting. 1305 is an overload of the pool's model, so once POOL_OVERLOAD_KEYS, 3, keys said it within a minute, step moves the bee to the next pool. After 8 attempts the turn ends.", + "task": "Mark key 2 as tried and see which key gets the ring; then set three keys that said 1305." + }, + "ru": { + "title": "Следующий ключ", + "goal": "Какой ключ пчела пробует следующим и когда переходит к другому пулу или сдаётся.", + "text": "На 2026-10-17 пчела начала на ключе 9 из 10 и погибла через 7 с, пока 33 других ключа простаивали: она получила один ключ при запуске и держала его. Теперь next_free берёт следующий после последнего ключ, который ещё не пробовали и который не отдыхает. 1305 — перегрузка модели пула, поэтому, когда POOL_OVERLOAD_KEYS, 3, ключа сказали это за минуту, step переводит пчелу в следующий пул. После 8 попыток ход заканчивается.", + "task": "Отметьте ключ 2 как опробованный и посмотрите, какой ключ получит кольцо; затем поставьте три ключа, ответивших 1305." + } + }, + { + "id": "no-credit-for-keys", + "n": 19, + "module": "credit-and-money", + "widget": { + "id": "hive-credit", + "kind": "tool", + "title": "Lending a key earns no credit: XP follows recorded work", + "url": "https://t27.ai/widgets/hive-credit/", + "preview": "widgets/hive-credit/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-price", + "kind": "tool", + "title": "The price of an operation: twice its cost, rounded up", + "url": "https://t27.ai/widgets/hive-price/", + "preview": "widgets/hive-price/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-next-key", + "kind": "tool", + "title": "The next key: a bee moves on instead of dying", + "url": "https://t27.ai/widgets/hive-next-key/", + "preview": "widgets/hive-next-key/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/queen-contributor-keys.t27", + "source": "t27/files/specs/automation/queen-contributor-keys.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Fqueen-contributor-keys.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "No credit for keys", + "goal": "What a contributor's lent keys earn, and which limits keep them safe.", + "text": "Anyone can lend the hive model keys through queen-contributor-keys.t27. A key is probed before it is enabled: at most 4 probes at once, a new probe waits out 60 s after the last, and an owner holds at most 100 keys. Lending earns nothing by itself: CREDIT_WALLET is false, so wallet_credit answers 0 for any number of keys. XP follows the work the Queen records.", + "task": "Drag the keys from 0 to 100 and watch the credit meter; then find the second a probe may start." + }, + "ru": { + "title": "Ключи не дают заслуг", + "goal": "Что приносят ключи, которые одолжил участник, и какие пределы их берегут.", + "text": "Любой может одолжить улью ключи моделей через queen-contributor-keys.t27. Ключ проверяют, прежде чем включить: не больше 4 проверок сразу, новая проверка ждёт 60 с после прошлой, а у одного владельца не больше 100 ключей. Сам по себе ключ ничего не приносит: CREDIT_WALLET ложно, поэтому wallet_credit отвечает 0 при любом числе ключей. Опыт следует за работой, которую записывает Королева.", + "task": "Протяните число ключей от 0 до 100 и посмотрите на счётчик заслуг; затем найдите секунду, когда можно начать проверку." + } + }, + { + "id": "twice-the-cost", + "n": 20, + "module": "credit-and-money", + "widget": { + "id": "hive-price", + "kind": "tool", + "title": "The price of an operation: twice its cost, rounded up", + "url": "https://t27.ai/widgets/hive-price/", + "preview": "widgets/hive-price/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-burn", + "kind": "tool", + "title": "Tokens that burn: a month to spend them", + "url": "https://t27.ai/widgets/hive-burn/", + "preview": "widgets/hive-burn/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-credit", + "kind": "tool", + "title": "Lending a key earns no credit: XP follows recorded work", + "url": "https://t27.ai/widgets/hive-credit/", + "preview": "widgets/hive-credit/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/token-wallet.t27", + "source": "t27/files/specs/automation/token-wallet.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Ftoken-wallet.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Twice the cost", + "goal": "How the project prices an operation and what a month of tokens buys.", + "text": "Every dollar becomes tokens at one rate, TOKEN_MICRO_USD 5000: a token is half a cent. price_tokens sells an operation at twice what it costs, MARKUP_PCT 200, and rounds up so nothing sells below that: a cost of 12300 micro-dollars is 5 tokens. month_tokens turns $20 into 4000 tokens, which is 14 reels of 270 tokens or 333 portraits of 12.", + "task": "Find the costs that give exactly 1 token; then find the monthly price that buys 36 reels." + }, + "ru": { + "title": "Вдвое дороже цены", + "goal": "Как проект назначает цену операции и что покупает месяц токенов.", + "text": "Каждый доллар становится токенами по одному курсу, TOKEN_MICRO_USD 5000: токен — полцента. price_tokens продаёт операцию вдвое дороже её стоимости, MARKUP_PCT 200, и округляет вверх, чтобы ничего не продавалось дешевле: стоимость 12300 микродолларов — это 5 токенов. month_tokens превращает $20 в 4000 токенов, то есть 14 рилсов по 270 токенов или 333 портрета по 12.", + "task": "Найдите стоимости, которые дают ровно 1 токен; затем найдите месячную цену, на которую выходит 36 рилсов." + } + }, + { + "id": "tokens-that-burn", + "n": 21, + "module": "credit-and-money", + "widget": { + "id": "hive-burn", + "kind": "tool", + "title": "Tokens that burn: a month to spend them", + "url": "https://t27.ai/widgets/hive-burn/", + "preview": "widgets/hive-burn/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-price", + "kind": "tool", + "title": "The price of an operation: twice its cost, rounded up", + "url": "https://t27.ai/widgets/hive-price/", + "preview": "widgets/hive-price/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-night", + "kind": "tool", + "title": "The night watch: four checks, one verdict, one report", + "url": "https://t27.ai/widgets/hive-night/", + "preview": "widgets/hive-night/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/token-wallet.t27", + "source": "t27/files/specs/automation/token-wallet.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Ftoken-wallet.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Tokens that burn", + "goal": "When a batch of tokens burns, and which batch is spent first.", + "text": "The owner's rule of 2026-10-01: all money goes into tokens, and tokens not spent within the month burn, so a person who paid makes content instead of saving tokens. Each batch lives MONTH_DAYS, 30 days, and days_left counts it down from the day it landed. On day 25 the package month from day 0 has 5 days left, the start gift from day 3 has 8, and a top-up from day 12 has 17. The spend order is soonest burn first.", + "task": "Find the day the package month burns; then find the last day any batch is left." + }, + "ru": { + "title": "Сгорающие токены", + "goal": "Когда сгорает партия токенов и какая партия тратится раньше.", + "text": "Правило владельца от 2026-10-01: все деньги уходят в токены, а токены, не потраченные за месяц, сгорают, так что заплативший делает контент, а не копит токены. Каждая партия живёт MONTH_DAYS, 30 дней, и days_left отсчитывает её от дня зачисления. На 25-й день у пакета месяца с дня 0 осталось 5 дней, у стартового подарка с дня 3 — 8, у пополнения с дня 12 — 17. Тратится раньше та, что сгорит раньше.", + "task": "Найдите день, когда сгорает пакет месяца; затем найдите последний день, когда остаётся хоть одна партия." + } + }, + { + "id": "who-is-called", + "n": 22, + "module": "agents-together", + "widget": { + "id": "hive-called", + "kind": "tool", + "title": "Who is called: an agent bot in a group of agents", + "url": "https://t27.ai/widgets/hive-called/", + "preview": "widgets/hive-called/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-chain", + "kind": "tool", + "title": "Chain depth: human, A, B, C, then silence", + "url": "https://t27.ai/widgets/hive-chain/", + "preview": "widgets/hive-chain/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-cooldown", + "kind": "tool", + "title": "The cooldown clock, /stop and /mute", + "url": "https://t27.ai/widgets/hive-cooldown/", + "preview": "widgets/hive-cooldown/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/agent-hive-group.t27", + "source": "t27/files/specs/automation/agent-hive-group.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Fagent-hive-group.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Who is called", + "goal": "Which messages in the hive group wake an agent, from a person and from another agent's bot.", + "text": "Every agent has its own bot and its own topic in one forum group. A person calls an agent by its tag, by @all, by a reply, or by writing in its topic. Another agent's bot calls it only by a tag or a reply: a bot never triggers the whole hive and never wakes a topic's owner by merely writing there. addressed in agent-hive-group.t27 is that rule, one line long.", + "task": "Find the two ways only a person can call an agent; then turn on all four and compare the senders." + }, + "ru": { + "title": "Кого зовут", + "goal": "Какие сообщения в группе улья будят агента — от человека и от бота другого агента.", + "text": "У каждого агента свой бот и своя тема в одной форумной группе. Человек зовёт агента его тегом, через @all, ответом на сообщение или просто сообщением в его теме. Бот другого агента зовёт его только тегом или ответом: бот никогда не поднимает весь улей и не будит хозяина темы, просто написав в ней. addressed в agent-hive-group.t27 — это правило длиной в одну строку.", + "task": "Найдите два способа, которыми агента может позвать только человек; затем включите все четыре и сравните отправителей." + } + }, + { + "id": "three-bots-deep", + "n": 23, + "module": "agents-together", + "widget": { + "id": "hive-chain", + "kind": "tool", + "title": "Chain depth: human, A, B, C, then silence", + "url": "https://t27.ai/widgets/hive-chain/", + "preview": "widgets/hive-chain/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-called", + "kind": "tool", + "title": "Who is called: an agent bot in a group of agents", + "url": "https://t27.ai/widgets/hive-called/", + "preview": "widgets/hive-called/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-cooldown", + "kind": "tool", + "title": "The cooldown clock, /stop and /mute", + "url": "https://t27.ai/widgets/hive-cooldown/", + "preview": "widgets/hive-cooldown/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/agent-hive-group.t27", + "source": "t27/files/specs/automation/agent-hive-group.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Fagent-hive-group.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Three bots deep", + "goal": "How the hive stops agents from answering each other forever.", + "text": "Telegram gives no loop protection between bots, so two agents could answer each other forever. Two numbers stop it. MAX_BOT_DEPTH is 3: a person, then A, B and C, then silence. BOT_REPLY_COOLDOWN_MS is 5000: a bot answers another bot at most once in 5 s. may_answer always lets a person through, and a bot only inside the chain and after the cooldown.", + "task": "Find the gap at which agent B falls silent; then explain why C is silent too." + }, + "ru": { + "title": "Три бота в глубину", + "goal": "Как улей не даёт агентам отвечать друг другу бесконечно.", + "text": "Telegram не защищает ботов от зацикливания, так что два агента могли бы отвечать друг другу вечно. Это останавливают два числа. MAX_BOT_DEPTH равно 3: человек, затем A, B и C, затем тишина. BOT_REPLY_COOLDOWN_MS равно 5000: бот отвечает другому боту не чаще раза в 5 с. may_answer всегда пропускает человека, а бота — только внутри цепочки и после паузы.", + "task": "Найдите промежуток, при котором агент B замолкает; затем объясните, почему молчит и C." + } + }, + { + "id": "stop-and-mute", + "n": 24, + "module": "agents-together", + "widget": { + "id": "hive-cooldown", + "kind": "tool", + "title": "The cooldown clock, /stop and /mute", + "url": "https://t27.ai/widgets/hive-cooldown/", + "preview": "widgets/hive-cooldown/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-chain", + "kind": "tool", + "title": "Chain depth: human, A, B, C, then silence", + "url": "https://t27.ai/widgets/hive-chain/", + "preview": "widgets/hive-chain/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-called", + "kind": "tool", + "title": "Who is called: an agent bot in a group of agents", + "url": "https://t27.ai/widgets/hive-called/", + "preview": "widgets/hive-called/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/agent-hive-group.t27", + "source": "t27/files/specs/automation/agent-hive-group.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Fagent-hive-group.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "Stop and mute", + "goal": "What /stop and /mute do, and why neither can lose a person or a direct call.", + "text": "cooled says whether 5000 ms have passed since the last bot message. /stop silences bot-to-bot turns in a chat for STOP_WINDOW_MS, 10 minutes, and a person is still heard: that is may_answer_after_stop. /mute keeps an agent quiet in a topic for MUTE_MS, an hour, but it still answers its own tag: heard_while_muted. Both are clocks, so nobody has to switch the hive back on.", + "task": "Drag the /stop clock past 10 minutes; then tag the muted agent." + }, + "ru": { + "title": "Stop и mute", + "goal": "Что делают /stop и /mute и почему ни один не теряет человека или прямой вызов.", + "text": "cooled говорит, прошло ли 5000 мс с последнего сообщения бота. /stop глушит разговоры ботов между собой в чате на STOP_WINDOW_MS, 10 минут, а человека по-прежнему слышно: это may_answer_after_stop. /mute заставляет агента молчать в теме MUTE_MS, час, но на свой тег он отвечает: heard_while_muted. Оба — часы, поэтому никому не нужно включать улей обратно.", + "task": "Протяните часы /stop дальше 10 минут; затем позовите тегом заглушённого агента." + } + }, + { + "id": "the-night-watch", + "n": 25, + "module": "night-and-the-board", + "widget": { + "id": "hive-night", + "kind": "tool", + "title": "The night watch: four checks, one verdict, one report", + "url": "https://t27.ai/widgets/hive-night/", + "preview": "widgets/hive-night/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-column", + "kind": "tool", + "title": "The public board: which column a task's card is in", + "url": "https://t27.ai/widgets/hive-column/", + "preview": "widgets/hive-column/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-cell", + "kind": "tool", + "title": "The comb answers: what an event does to a cell", + "url": "https://t27.ai/widgets/hive-cell/", + "preview": "widgets/hive-cell/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/automation/card-chat-night-watch.t27", + "source": "t27/files/specs/automation/card-chat-night-watch.t27", + "preview": "play/embed.html?spec=specs%2Fautomation%2Fcard-chat-night-watch.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The night watch", + "goal": "What the night watch does in the morning with four card checks.", + "text": "Every night four checks of the card chat run: the thread, the status door, the merges and the parity. night_verdict sums them up: any fail is a fail, else anything unmeasured is unmeasured, else it is a pass. night_report turns that into one act: a fail opens an issue or comments on the open one, a pass closes an open issue or stays silent, and an unmeasured night leaves everything as it is.", + "task": "Make the night pass with the issue open; then make one check unmeasured." + }, + "ru": { + "title": "Ночной дозор", + "goal": "Что ночной дозор делает утром с четырьмя проверками карточек.", + "text": "Каждую ночь идут четыре проверки чата карточек: тред, дверь статуса, слияния и паритет. night_verdict подводит итог: любое падение — это падение, иначе любое неизмеренное — неизмерено, иначе всё прошло. night_report превращает это в одно действие: падение открывает задачу или комментирует открытую, успех закрывает открытую задачу или молчит, а неизмеренная ночь оставляет всё как есть.", + "task": "Сделайте ночь успешной при открытой задаче; затем сделайте одну проверку неизмеренной." + } + }, + { + "id": "the-public-board", + "n": 26, + "module": "night-and-the-board", + "widget": { + "id": "hive-column", + "kind": "tool", + "title": "The public board: which column a task's card is in", + "url": "https://t27.ai/widgets/hive-column/", + "preview": "widgets/hive-column/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-task-filter", + "kind": "tool", + "title": "One task, three kinds: the filter the board and the game share", + "url": "https://t27.ai/widgets/hive-task-filter/", + "preview": "widgets/hive-task-filter/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-lifecycle", + "kind": "tool", + "title": "A task's life: the Queen's state machine", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-night", + "kind": "tool", + "title": "The night watch: four checks, one verdict, one report", + "url": "https://t27.ai/widgets/hive-night/", + "preview": "widgets/hive-night/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/dispatch.t27", + "source": "t27/files/specs/queen/dispatch.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fdispatch.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The public board", + "goal": "Which column the public board draws each claim in, and the case it gets wrong.", + "text": "The board at app.t27.ai/game/kanban draws every dispatch in one of 6 columns. column_of reads the claim a dispatch holds on its issue: running is running, accepted is done, rejected and awaiting review are in review, and failed is dropped. dispatch.t27 records one finding as it is: once an issue is closed and the list was read, every finished claim is drawn as done, whatever its verdict.", + "task": "Open the issue and see where the four finished claims go; then say what a closed issue hides." + }, + "ru": { + "title": "Публичная доска", + "goal": "В какой колонке публичная доска рисует каждое притязание и в каком случае ошибается.", + "text": "Доска на app.t27.ai/game/kanban рисует каждый запуск в одной из 6 колонок. column_of читает притязание запуска на его задачу: running — в работе, accepted — готово, rejected и awaiting review — на ревью, failed — сброшено. dispatch.t27 записывает одну находку как есть: когда задача закрыта и список прочитан, любое завершённое притязание рисуется как готовое, каким бы ни был вердикт.", + "task": "Откройте задачу и посмотрите, куда уйдут четыре завершённых притязания; затем скажите, что скрывает закрытая задача." + } + }, + { + "id": "the-comb-answers", + "n": 27, + "module": "night-and-the-board", + "widget": { + "id": "hive-cell", + "kind": "tool", + "title": "The comb answers: what an event does to a cell", + "url": "https://t27.ai/widgets/hive-cell/", + "preview": "widgets/hive-cell/index.html?embed=1", + "height": 760 + }, + "also": [ + { + "id": "hive-ship", + "kind": "tool", + "title": "The game: where a bee flies and how it leaves", + "url": "https://t27.ai/widgets/hive-ship/", + "preview": "widgets/hive-ship/index.html?embed=1", + "height": 760 + }, + { + "id": "hive-night", + "kind": "tool", + "title": "The night watch: four checks, one verdict, one report", + "url": "https://t27.ai/widgets/hive-night/", + "preview": "widgets/hive-night/index.html?embed=1", + "height": 760 + } + ], + "spec": { + "path": "specs/queen/hive.t27", + "source": "t27/files/specs/queen/hive.t27", + "preview": "play/embed.html?spec=specs%2Fqueen%2Fhive.t27&tab=term&v=7cbe9ab0c960", + "height": 480 + }, + "en": { + "title": "The comb answers", + "goal": "How the game shows that something happened to a task, and how often the map grows.", + "text": "The game's comb has a cell for each issue, and before hive.t27 an accepted task, a lost lease and a cancel looked the same as no news. cell_effect gives each of the 10 event kinds its paint: a start white, an accepting review gold, a send-back amber, a lost lease red, a cancel grey. flash_alpha fades it over FLASH_MS, 1800 ms. A new cell rebuilds the scene, so cells_due places new cells in batches at most every 300 s.", + "task": "Tap the review cell and switch the verdict; then find the moment the flash is gone." + }, + "ru": { + "title": "Соты отвечают", + "goal": "Как игра показывает, что с задачей что-то случилось, и как часто растёт карта.", + "text": "В сотах игры у каждой задачи своя ячейка, и до hive.t27 принятая задача, потерянная аренда и отмена выглядели так же, как отсутствие новостей. cell_effect даёт каждому из 10 видов событий свою краску: старт белый, принявшее ревью золотое, возврат янтарный, потерянная аренда красная, отмена серая. flash_alpha гасит вспышку за FLASH_MS, 1800 мс. Новая ячейка перестраивает сцену, поэтому cells_due ставит новые ячейки пачками не чаще раза в 300 с.", + "task": "Нажмите ячейку ревью и переключите вердикт; затем найдите момент, когда вспышка гаснет." + } + } + ], + "prev": { + "id": "neural-networks", + "route": "neural-networks", + "share": "learn/neural-networks/", + "first": "relu", + "last": "ppo-and-the-reward", + "title": { + "en": "Neural networks in t27: from one neuron to a transformer block", + "ru": "Нейросети на t27: от одного нейрона до блока трансформера" + } + }, "next": null } ] as const diff --git a/apps/website/src/lib/courseRoutes.generated.ts b/apps/website/src/lib/courseRoutes.generated.ts index 66a4753315..c1774ec76f 100644 --- a/apps/website/src/lib/courseRoutes.generated.ts +++ b/apps/website/src/lib/courseRoutes.generated.ts @@ -2,4 +2,4 @@ // Do not edit: change the .t27 source and regenerate. /** Every course's address in the app (#/, #//), in reading order. */ -export const COURSE_ROUTES = ["t27-basics","course","ai-numbers","goldenfloat","verify-hardware","clocks-and-cdc","buses-and-peripherals","ternary-computing","arithmetic-and-dsp","neural-networks"] as const +export const COURSE_ROUTES = ["t27-basics","course","ai-numbers","goldenfloat","verify-hardware","clocks-and-cdc","buses-and-peripherals","ternary-computing","arithmetic-and-dsp","neural-networks","queen-and-hive"] as const diff --git a/apps/website/src/lib/queenWidgets.generated.ts b/apps/website/src/lib/queenWidgets.generated.ts index ec049a5412..09d2398a44 100644 --- a/apps/website/src/lib/queenWidgets.generated.ts +++ b/apps/website/src/lib/queenWidgets.generated.ts @@ -1,12 +1,12 @@ // GENERATED by scripts/widgets-from-spec.mjs from specs/widgets/gallery.t27 -// spec sha256 1f89127fb05172d18af3b035147793a5729c85ef722609555e2e7de68d43b7eb +// spec sha256 7a76e1f96945b66fd852b5397dc62dcccf2bec6628bf658009bd1a4d40fbe514 // Do not edit: change the .t27 source and regenerate. export const QUEEN_WIDGETS = { "source": { "spec": "specs/widgets/gallery.t27", "publicSpec": "public/widgets/gallery.t27", - "sha256": "1f89127fb05172d18af3b035147793a5729c85ef722609555e2e7de68d43b7eb", + "sha256": "7a76e1f96945b66fd852b5397dc62dcccf2bec6628bf658009bd1a4d40fbe514", "schemaVersion": 1 }, "name": "t27 widgets", @@ -5014,6 +5014,465 @@ export const QUEEN_WIDGETS = { "iframe": "", "markdown": "[![PPO: how an agent learns from reward without lurching](https://t27.ai/widgets/nn-ppo/card.png?v=432b62b0ecd7)](https://t27.ai/widgets/nn-ppo/)" }, + { + "id": "hive-shape", + "kind": "tool", + "category": "game", + "hook": "Two failed shapes and the Queen stops rewriting the issue: she comments once with what is missing, and never again.", + "title": "Shaping: what the Queen does with an issue that is not ready", + "line": "Set how many sections an issue lacks and how often shaping failed: the Queen leaves it, shapes it, or tells its author once what is missing.", + "url": "https://t27.ai/widgets/hive-shape/", + "view": null, + "image": "https://t27.ai/widgets/hive-shape/card.png?v=9de5735a6808", + "gif": null, + "preview": "widgets/hive-shape/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Shaping: what the Queen does with an issue that is not ready](https://t27.ai/widgets/hive-shape/card.png?v=9de5735a6808)](https://t27.ai/widgets/hive-shape/)" + }, + { + "id": "hive-boundary", + "kind": "tool", + "category": "game", + "hook": "A wrong boundary is worse than a missing one: it reserves files the work does not own. An invented path is refused, and one refusal stops the shape.", + "title": "A boundary: which files a task may reserve", + "line": "Four paths a model proposed for an issue. Tap what is true of each: the spec accepts or refuses it, and a shape lands only when every path holds.", + "url": "https://t27.ai/widgets/hive-boundary/", + "view": null, + "image": "https://t27.ai/widgets/hive-boundary/card.png?v=522169f68813", + "gif": null, + "preview": "widgets/hive-boundary/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![A boundary: which files a task may reserve](https://t27.ai/widgets/hive-boundary/card.png?v=522169f68813)](https://t27.ai/widgets/hive-boundary/)" + }, + { + "id": "hive-task-filter", + "kind": "tool", + "category": "game", + "hook": "An issue, a job and a review each kept their own codes. Now one filter, a bit per kind and a bit per state, serves the board, the game and the route.", + "title": "One task, three kinds: the filter the board and the game share", + "line": "An issue, a review and a job are one kind of task with seven states. Tap kinds and states: the grid lights every pair task_matches lets through.", + "url": "https://t27.ai/widgets/hive-task-filter/", + "view": null, + "image": "https://t27.ai/widgets/hive-task-filter/card.png?v=d7f837a5290c", + "gif": null, + "preview": "widgets/hive-task-filter/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![One task, three kinds: the filter the board and the game share](https://t27.ai/widgets/hive-task-filter/card.png?v=d7f837a5290c)](https://t27.ai/widgets/hive-task-filter/)" + }, + { + "id": "hive-aging", + "kind": "tool", + "category": "game", + "hook": "A low label that aged past unlabelled work would make low the fastest way to the front. So aging is one step, and only for labelled issues.", + "title": "Aging: how long a labelled low issue waits", + "line": "Pick a level, label it or not, and drag its age: a labelled issue gains one level after 14 days, at most one, and never reaches critical.", + "url": "https://t27.ai/widgets/hive-aging/", + "view": null, + "image": "https://t27.ai/widgets/hive-aging/card.png?v=9aacfb6a19ae", + "gif": null, + "preview": "widgets/hive-aging/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Aging: how long a labelled low issue waits](https://t27.ai/widgets/hive-aging/card.png?v=9aacfb6a19ae)](https://t27.ai/widgets/hive-aging/)" + }, + { + "id": "hive-rank", + "kind": "tool", + "category": "game", + "hook": "Labels can inflate: at most 3 criticals run as critical, the fourth runs as high. An issue with an open blocker is skipped, not waited on.", + "title": "The queue: which issue the Queen takes first", + "line": "Seven open issues in GitHub order. Tap one to change its label: the cap, aging, blockers and outranks reorder the queue, and each issue says why it sits there.", + "url": "https://t27.ai/widgets/hive-rank/", + "view": null, + "image": "https://t27.ai/widgets/hive-rank/card.png?v=656916c63db4", + "gif": null, + "preview": "widgets/hive-rank/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The queue: which issue the Queen takes first](https://t27.ai/widgets/hive-rank/card.png?v=656916c63db4)](https://t27.ai/widgets/hive-rank/)" + }, + { + "id": "hive-review-first", + "kind": "tool", + "category": "game", + "hook": "Measured 2026-10-18: nine bees started for every verdict, so the review queue only grew. Now the backlog closes while anything waits in review.", + "title": "Review first: no new work while finished work waits", + "line": "Drag the review queue. While anything waits in review, a backlog issue is refused; work the review sent back and work the owner assigned still go.", + "url": "https://t27.ai/widgets/hive-review-first/", + "view": null, + "image": "https://t27.ai/widgets/hive-review-first/card.png?v=a16ace665d71", + "gif": null, + "preview": "widgets/hive-review-first/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Review first: no new work while finished work waits](https://t27.ai/widgets/hive-review-first/card.png?v=a16ace665d71)](https://t27.ai/widgets/hive-review-first/)" + }, + { + "id": "hive-lifecycle", + "kind": "tool", + "category": "game", + "hook": "A row has no status column: its state is read from four fields. This machine is the one that chooses, starts, ends, reviews and releases work.", + "title": "A task's life: the Queen's state machine", + "line": "Twelve states and sixteen events. Tap an event: next_state moves the task, and every event it would refuse is greyed out.", + "url": "https://t27.ai/widgets/hive-lifecycle/", + "view": null, + "image": "https://t27.ai/widgets/hive-lifecycle/card.png?v=f61aa595dacf", + "gif": null, + "preview": "widgets/hive-lifecycle/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![A task's life: the Queen's state machine](https://t27.ai/widgets/hive-lifecycle/card.png?v=f61aa595dacf)](https://t27.ai/widgets/hive-lifecycle/)" + }, + { + "id": "hive-ship", + "kind": "tool", + "category": "game", + "hook": "Before this spec a bee that left the feed vanished in the frame it left. Now it flies home and fades over 2400 ms, so a finished task has an ending on screen.", + "title": "The game: where a bee flies and how it leaves", + "line": "Set what the bee is doing and where its cell stands: ship_mode puts it at the entrance, the portal, its cell or on the way home, and ship_alpha fades it out.", + "url": "https://t27.ai/widgets/hive-ship/", + "view": null, + "image": "https://t27.ai/widgets/hive-ship/card.png?v=93cae90fafd5", + "gif": null, + "preview": "widgets/hive-ship/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The game: where a bee flies and how it leaves](https://t27.ai/widgets/hive-ship/card.png?v=93cae90fafd5)](https://t27.ai/widgets/hive-ship/)" + }, + { + "id": "hive-drain", + "kind": "tool", + "category": "game", + "hook": "A deploy does not cut a bee short: it is held up to the cap, then what it wrote is salvaged into its branch and its issue is free at once.", + "title": "A deploy mid-flight: what happens to a running bee", + "line": "The Queen is redeployed while work runs. Drag how long the drain has waited: a bee is held to the cap and then handed back, a review is stopped, a round and a job are left to the store.", + "url": "https://t27.ai/widgets/hive-drain/", + "view": null, + "image": "https://t27.ai/widgets/hive-drain/card.png?v=435b182a0ea0", + "gif": null, + "preview": "widgets/hive-drain/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![A deploy mid-flight: what happens to a running bee](https://t27.ai/widgets/hive-drain/card.png?v=435b182a0ea0)](https://t27.ai/widgets/hive-drain/)" + }, + { + "id": "hive-reviewers", + "kind": "tool", + "category": "game", + "hook": "At 300 s a review, one slot clears 12 rows an hour. The fixed 4 tops out at 48 while bees finish 58, so the backlog only grew. 16 lanes keep up.", + "title": "How many reviewer bees: keeping up with the finish rate", + "line": "Set the keys, lanes, memory and the rows waiting: reviewer_concurrency says how many reviews run at once, and keeps_up says whether that outruns the bees finishing work.", + "url": "https://t27.ai/widgets/hive-reviewers/", + "view": null, + "image": "https://t27.ai/widgets/hive-reviewers/card.png?v=3fd84e8d5339", + "gif": null, + "preview": "widgets/hive-reviewers/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![How many reviewer bees: keeping up with the finish rate](https://t27.ai/widgets/hive-reviewers/card.png?v=3fd84e8d5339)](https://t27.ai/widgets/hive-reviewers/)" + }, + { + "id": "hive-revisit", + "kind": "tool", + "category": "game", + "hook": "Measured over 44 minutes: 941 visits, 888 of them wait, from 22 rows. One row was visited 54 times. Doubling the pause gave those slots back.", + "title": "Back off: when the reviewer looks at a waiting row again", + "line": "A row that answered wait is not visited again at once: 30 s, doubled per wait, never more than 900 s. Drag the waits and the clock and see when it is due.", + "url": "https://t27.ai/widgets/hive-revisit/", + "view": null, + "image": "https://t27.ai/widgets/hive-revisit/card.png?v=53f31c875c8b", + "gif": null, + "preview": "widgets/hive-revisit/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Back off: when the reviewer looks at a waiting row again](https://t27.ai/widgets/hive-revisit/card.png?v=53f31c875c8b)](https://t27.ai/widgets/hive-revisit/)" + }, + { + "id": "hive-gate", + "kind": "tool", + "category": "game", + "hook": "Zero checks is not zero failures: a pull request with no posted check does not merge. And a red check counts as required whenever the ruleset cannot be read.", + "title": "The merge gate: when an approved pull request may merge", + "line": "Four checks on a pull request the reviewer bee approved. Tap what each one is: check_passes judges it, and gate_open opens only when one has posted, none runs and none blocks.", + "url": "https://t27.ai/widgets/hive-gate/", + "view": null, + "image": "https://t27.ai/widgets/hive-gate/card.png?v=1f1e633685fe", + "gif": null, + "preview": "widgets/hive-gate/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The merge gate: when an approved pull request may merge](https://t27.ai/widgets/hive-gate/card.png?v=1f1e633685fe)](https://t27.ai/widgets/hive-gate/)" + }, + { + "id": "hive-jam", + "kind": "tool", + "category": "game", + "hook": "A pull request can be refused, pushed again and red at once. The earliest rule that matches wins, so the Queen always knows which one case she is in.", + "title": "The review jam: which case a stuck pull request is in", + "line": "Nine facts about a pull request that will not move. Toggle them: the earliest rule that matches wins, and the pull request lands on that rule.", + "url": "https://t27.ai/widgets/hive-jam/", + "view": null, + "image": "https://t27.ai/widgets/hive-jam/card.png?v=66296b2e3048", + "gif": null, + "preview": "widgets/hive-jam/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The review jam: which case a stuck pull request is in](https://t27.ai/widgets/hive-jam/card.png?v=66296b2e3048)](https://t27.ai/widgets/hive-jam/)" + }, + { + "id": "hive-jam-action", + "kind": "tool", + "category": "game", + "hook": "An unknown case is never nothing to do: anything above 9 maps to measure again. A queue that cannot be read is not reported as free.", + "title": "The Queen's move: what each jam asks of her", + "line": "Each jam case has one action at the same position. Drag the case: jam_action names the move, and jam_is_queens_turn splits the board into her turn and waiting.", + "url": "https://t27.ai/widgets/hive-jam-action/", + "view": null, + "image": "https://t27.ai/widgets/hive-jam-action/card.png?v=2b6d86f79093", + "gif": null, + "preview": "widgets/hive-jam-action/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The Queen's move: what each jam asks of her](https://t27.ai/widgets/hive-jam-action/card.png?v=2b6d86f79093)](https://t27.ai/widgets/hive-jam-action/)" + }, + { + "id": "hive-master-red", + "kind": "tool", + "category": "game", + "hook": "On 2026-10-06 a PR merged red: master's run was cancelled and read as red. A cancelled run says nothing about the code; only a completed failure counts.", + "title": "Inherited red: what master's runs say about a red check", + "line": "The bee says a red check is red on master too. The merger reads master itself, newest run first: a cancelled run is looked past, and only a failure at or after the merge base discounts.", + "url": "https://t27.ai/widgets/hive-master-red/", + "view": null, + "image": "https://t27.ai/widgets/hive-master-red/card.png?v=d300100a9a05", + "gif": null, + "preview": "widgets/hive-master-red/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Inherited red: what master's runs say about a red check](https://t27.ai/widgets/hive-master-red/card.png?v=d300100a9a05)](https://t27.ai/widgets/hive-master-red/)" + }, + { + "id": "hive-key-error", + "kind": "tool", + "category": "game", + "hook": "z.ai sends a spent quota and a passing overload with the same 429. So the code in the body wins over the status, and 1305 never closes a key.", + "title": "A refused key: transient, quota, auth or a bad request", + "line": "Pick the HTTP status and the z.ai code a bee got back. error_kind sorts the refusal, and the grid colours every status against every code the same way.", + "url": "https://t27.ai/widgets/hive-key-error/", + "view": null, + "image": "https://t27.ai/widgets/hive-key-error/card.png?v=08438a500d62", + "gif": null, + "preview": "widgets/hive-key-error/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![A refused key: transient, quota, auth or a bad request](https://t27.ai/widgets/hive-key-error/card.png?v=08438a500d62)](https://t27.ai/widgets/hive-key-error/)" + }, + { + "id": "hive-key-rest", + "kind": "tool", + "category": "game", + "hook": "Long enough that the next bee does not hit the same overloaded key at once, short enough that 34 keys are never all resting through a one-minute burst.", + "title": "How long a refused key rests", + "line": "A transient refusal rests the key 15 s, doubled per strike in a row, never past 240 s. A spent quota or a refused key closes it for 1800 s. Drag the strikes.", + "url": "https://t27.ai/widgets/hive-key-rest/", + "view": null, + "image": "https://t27.ai/widgets/hive-key-rest/card.png?v=d2ee04809d49", + "gif": null, + "preview": "widgets/hive-key-rest/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![How long a refused key rests](https://t27.ai/widgets/hive-key-rest/card.png?v=d2ee04809d49)](https://t27.ai/widgets/hive-key-rest/)" + }, + { + "id": "hive-next-key", + "kind": "tool", + "category": "game", + "hook": "Measured 2026-10-17: a bee died 7 s into its turn on key 9 of 10 while 33 keys stood by. Now it tries the next free key, never the one that failed.", + "title": "The next key: a bee moves on instead of dying", + "line": "A pool of keys, some tried, some resting. Tap keys to change them: next_free picks the next one after the last, and step says next key, next pool or give up.", + "url": "https://t27.ai/widgets/hive-next-key/", + "view": null, + "image": "https://t27.ai/widgets/hive-next-key/card.png?v=e9b9457ac966", + "gif": null, + "preview": "widgets/hive-next-key/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The next key: a bee moves on instead of dying](https://t27.ai/widgets/hive-next-key/card.png?v=e9b9457ac966)](https://t27.ai/widgets/hive-next-key/)" + }, + { + "id": "hive-credit", + "kind": "tool", + "category": "game", + "hook": "Adding a key never credits a wallet: CREDIT_WALLET is false in the spec, so XP follows only the work the Queen records, not the keys someone lends.", + "title": "Lending a key earns no credit: XP follows recorded work", + "line": "A contributor lends the hive model keys. Drag the keys added: the wallet credit meter stays at 0, a key slot opens up to 100, and a probe waits out its 60 s.", + "url": "https://t27.ai/widgets/hive-credit/", + "view": null, + "image": "https://t27.ai/widgets/hive-credit/card.png?v=2cbd1526a6e6", + "gif": null, + "preview": "widgets/hive-credit/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Lending a key earns no credit: XP follows recorded work](https://t27.ai/widgets/hive-credit/card.png?v=2cbd1526a6e6)](https://t27.ai/widgets/hive-credit/)" + }, + { + "id": "hive-price", + "kind": "tool", + "category": "game", + "hook": "One rate everywhere: a token is half a cent, and every operation is sold at twice what it costs, rounded up so nothing is sold below that.", + "title": "The price of an operation: twice its cost, rounded up", + "line": "Drag what an operation costs the project and what a person pays a month: price_tokens sells it at twice the cost in half-cent tokens, and month_tokens turns dollars into tokens.", + "url": "https://t27.ai/widgets/hive-price/", + "view": null, + "image": "https://t27.ai/widgets/hive-price/card.png?v=e0263cf37c4e", + "gif": null, + "preview": "widgets/hive-price/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The price of an operation: twice its cost, rounded up](https://t27.ai/widgets/hive-price/card.png?v=e0263cf37c4e)](https://t27.ai/widgets/hive-price/)" + }, + { + "id": "hive-burn", + "kind": "tool", + "category": "game", + "hook": "All money goes into tokens, and the tokens burn within the month: a person who paid makes content instead of saving tokens.", + "title": "Tokens that burn: a month to spend them", + "line": "Three batches of tokens, each burning 30 days after it lands. Drag today across two months: days_left counts down every batch, and the one that burns soonest is spent first.", + "url": "https://t27.ai/widgets/hive-burn/", + "view": null, + "image": "https://t27.ai/widgets/hive-burn/card.png?v=14a6c28a877e", + "gif": null, + "preview": "widgets/hive-burn/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Tokens that burn: a month to spend them](https://t27.ai/widgets/hive-burn/card.png?v=14a6c28a877e)](https://t27.ai/widgets/hive-burn/)" + }, + { + "id": "hive-called", + "kind": "tool", + "category": "game", + "hook": "A bot never triggers the whole hive and never wakes a topic's owner by merely writing there. Only a tag or a reply calls an agent from another bot.", + "title": "Who is called: an agent bot in a group of agents", + "line": "Each agent has its own bot and topic in one group. Toggle how a message reaches an agent: addressed answers for a person and for another agent's bot.", + "url": "https://t27.ai/widgets/hive-called/", + "view": null, + "image": "https://t27.ai/widgets/hive-called/card.png?v=66bcb9b5af0a", + "gif": null, + "preview": "widgets/hive-called/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Who is called: an agent bot in a group of agents](https://t27.ai/widgets/hive-called/card.png?v=66bcb9b5af0a)](https://t27.ai/widgets/hive-called/)" + }, + { + "id": "hive-chain", + "kind": "tool", + "category": "game", + "hook": "Telegram has no loop protection between bots. Two numbers stop two agents answering each other forever: a depth of 3 and a cooldown of 5 s.", + "title": "Chain depth: human, A, B, C, then silence", + "line": "One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it.", + "url": "https://t27.ai/widgets/hive-chain/", + "view": null, + "image": "https://t27.ai/widgets/hive-chain/card.png?v=3c5a08a2c048", + "gif": null, + "preview": "widgets/hive-chain/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![Chain depth: human, A, B, C, then silence](https://t27.ai/widgets/hive-chain/card.png?v=3c5a08a2c048)](https://t27.ai/widgets/hive-chain/)" + }, + { + "id": "hive-cooldown", + "kind": "tool", + "category": "game", + "hook": "A stop cuts bot-to-bot turns only, a mute still answers its own tag: neither one can lose a person or a direct call.", + "title": "The cooldown clock, /stop and /mute", + "line": "One agent answers another at most every 5 s. /stop silences bot-to-bot turns for 10 minutes and /mute an agent for an hour. Drag the clocks: a person is always heard.", + "url": "https://t27.ai/widgets/hive-cooldown/", + "view": null, + "image": "https://t27.ai/widgets/hive-cooldown/card.png?v=4f75ab51c9ca", + "gif": null, + "preview": "widgets/hive-cooldown/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The cooldown clock, /stop and /mute](https://t27.ai/widgets/hive-cooldown/card.png?v=4f75ab51c9ca)](https://t27.ai/widgets/hive-cooldown/)" + }, + { + "id": "hive-night", + "kind": "tool", + "category": "game", + "hook": "Any fail is a fail; else anything unmeasured leaves things as they are. A night that could not measure never closes an issue.", + "title": "The night watch: four checks, one verdict, one report", + "line": "Every night four card checks run. Tap each to pass, fail or unmeasured: night_verdict sums them up and night_report says whether to open an issue, comment, close it or stay silent.", + "url": "https://t27.ai/widgets/hive-night/", + "view": null, + "image": "https://t27.ai/widgets/hive-night/card.png?v=8d76f4215b34", + "gif": null, + "preview": "widgets/hive-night/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The night watch: four checks, one verdict, one report](https://t27.ai/widgets/hive-night/card.png?v=8d76f4215b34)](https://t27.ai/widgets/hive-night/)" + }, + { + "id": "hive-column", + "kind": "tool", + "category": "game", + "hook": "The spec records it as a finding: the public board draws a closed issue's dispatch as done whatever its verdict, an escalation and a send-back included.", + "title": "The public board: which column a task's card is in", + "line": "Five claims a dispatch can hold on its issue. Close the issue: column_of moves every finished one to done, whatever its verdict.", + "url": "https://t27.ai/widgets/hive-column/", + "view": null, + "image": "https://t27.ai/widgets/hive-column/card.png?v=fb212a6061ea", + "gif": null, + "preview": "widgets/hive-column/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The public board: which column a task's card is in](https://t27.ai/widgets/hive-column/card.png?v=fb212a6061ea)](https://t27.ai/widgets/hive-column/)" + }, + { + "id": "hive-cell", + "kind": "tool", + "category": "game", + "hook": "Before this spec an accepted task, a lost lease and a cancel looked the same as no news. Now each paints its cell, and the flash fades over 1800 ms.", + "title": "The comb answers: what an event does to a cell", + "line": "Ten kinds of event reach the game. Pick one and the moment since: cell_effect colours the cell gold, amber, red, grey or white, flash_alpha fades it, and cells_due batches new cells.", + "url": "https://t27.ai/widgets/hive-cell/", + "view": null, + "image": "https://t27.ai/widgets/hive-cell/card.png?v=5d43c8b32efa", + "gif": null, + "preview": "widgets/hive-cell/index.html?embed=1", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![The comb answers: what an event does to a cell](https://t27.ai/widgets/hive-cell/card.png?v=5d43c8b32efa)](https://t27.ai/widgets/hive-cell/)" + }, { "id": "wars", "kind": "tab",