Перенос оракула яруса III — не одна задача, а пять разных, и две из них другого рода
В docs/javascript-removal.md ярус оракулов записан как ОДНА задача: «перенести оракула в дерево, образец есть и применён дважды». Разведка по коду 19 августа это опровергает. Образец (self/oracle.flang 441 строка + self/law-oracle.flang 370, второй берёт общее у первого ввозом) годится для трёх файлов из пяти. Для двух он не годится вовсе, и причина у каждого своя.
Чем подтверждено. Прочитаны оракулы в self-functor.test.mjs (строки 140-338), self-grid.test.mjs (199-300) и общая часть self/oracle.flang (145-270). Ветка work/udalenie-js поверх ствола f8f9c8e2.
Что общего у образца и берётся ввозом, а не копией
oracle.flang держит машину, которой хватает любому оракулу вызовов: объект «Ход оракула» (строки + готовая программа + программа), «Записать вызов» (один ответ на ключ, ключ строит сам закон), «Спросить вычислитель», «Положить ответ», «Без повторов», «Сетка оракула», «Строки хода», «Предел оракула» (сетка закона плюс четыре). Третьей копии этого быть не должно.
Три файла, которым образец подходит: functor, setoid, sets (половина)
У них оракул — замыкание вызовов: перебрать значения сетки, позвать названные функции, записать ответ. Ровно то, что делают «Замкнуть коммутативность» и «Замкнуть изоморфизм».
Замыкание функтора разложено (self-functor.test.mjs, 257-337) и состоит из трёх вещей, каждая выразима: «применить стрелку» (единица отдаёт вход, композиция зовёт правую потом левую, обычная стрелка пишет вызов gives), «концы стрелки» (domain/codomain, у композиции — от правой начало, от левой конец) и «сетка объекта» (значения примеров перевода, плюс входы стрелок с этим доменом, плюс ожидаемые стрелок с этим кодоменом). Рекурсия у первых двух — с пределом глубины 32, то есть объявленной мерой, а не структурой.
Два файла другого рода, и сверка у них строится иначе
grid (201 строка свидетеля). Его оракул не зовёт функцию — он строит тень программы, поднимает на ней среду и ловит отказ прогона примера (self-grid.test.mjs, 263-296: createRuntime в try, затем среда.call в try, код отказа достаётся у ошибки либо у первой диагностики). Отказ здесь — исключение хозяина, а не значение; исключений у языка нет by design. Значит сверка строится не «эталон против свидетеля на общей таблице», а так: тень уже считает сторона на flang («Тень программы»), а прогон по тени остаётся за хозяином, и переносить надо не оракула, а границу — либо interpret.flang должен отдавать отказ прогона значением. Это отдельная задача, и она больше переноса.
sets (503 строки). Вторая половина, checkSetShapes, берёт контекст у типизатора (types.mjs зовёт её с ctx), а не программу целиком. Отдельно от типизатора она не снимается и не сверяется. Значит sets.mjs уходит не целиком: checkSetLaws переносится по образцу, а checkSetShapes уезжает вместе с types.mjs — тем же разделением, каким выехал obligations.mjs.
Почему это важно записать
Запись «ярус III — ОДНА задача» занижает работу примерно вдвое и, что хуже, обещает, что образец закроет её весь. Он закроет три файла из пяти (functor 362, setoid 785, половина sets), то есть около 1 400 строк из 2 299. Оставшиеся ~900 — две задачи другого рода, и ни одна не решается повторением образца.
Чем ограничено. Разложение снято чтением кода, а не прогоном переноса: числа строк точные, оценка «выразимо» у функтора — по разбору форм, и её подтвердит только написанный и сверенный файл.
Побочная находка: запись ответов на растущем корпусе даёт ложный красный
Первая редакция записи (flang/test/fixtures/zapis-oracula.json) считала отпечаток по СЕГОДНЯШНЕМУ корпусу. Через час ствол влил новую программу (shortener/plan-durable.flang), и отпечаток покраснел, хотя ответы яруса не менялись ни на одной программе. Ложный красный хуже отсутствующей проверки: его быстро научаются не читать.
Исправлено так: запись помнит СВОЙ список путей и сверяется ровно по нему. Исчезновение записанной программы краснеет; появление новой — нет, про рост корпуса говорит отдельное утверждение числом. Правило общее для любой замороженной записи: замораживать надо и ответы, и список входов, иначе проверка меряет чужую работу.
Чем подтверждено. Прогон flang/test/zapis-oracula.test.mjs до правки — 1 красный из 8 на отпечатке 54f7b210… против записанного 524a06c4…; после правки 9 из 9 зелёных, запись 285 программ.
Поправка 19 августа: разложение подтвердилось прогоном, но одна цифра была не та
Оценка «образец закроет три файла из пяти, около 1 400 строк» проверена переносом, а не чтением, и подтвердилась по СОСТАВУ: setoid (785) лёг на образец без единой неожиданности — оракул написан, проверен и сверен побайтово с первой попытки; sets разделился ровно там, где сказано.
Не подтвердилось другое — скрытое допущение, что переключение всегда опускает потолок. У setoid оно верно: рабочий путь 28 файлов / 37 263 строки → 27 / 36 581. У sets — нет: 36 840 → 36 915, потолок ВЫРОС на 75, потому что checkSetShapes держит модуль в загрузке, даже когда checkSetLaws больше ничего не решает. Разбор — в отдельной заметке.
Ещё уточнение по числам: остаток sets не «~250 строк», как оценивалось здесь по половине файла, а 278 — считано по строкам, а не делением пополам (checkSetLaws со своим хозяйством занимает 225).
Связано: javascript-stays-only-as-a-print-target, witness-answers-must-be-recorded-before-the-deletion, switching-a-layer-does-not-always-lower-the-ceiling