This repository was archived by the owner on Oct 6, 2026. It is now read-only.
Repository navigation
fix: spec 024 tail — config contract, D28 root, and four of its own regressions - #4
Merged
Merged
Conversation
validateHarnessConfig проверял ВОСЕМЬ полей из тридцати трёх, которые читает
код. Остальные при опечатке не отвергались, а МЕНЯЛИ ПОВЕДЕНИЕ ГЕЙТА (D36):
validateHarnessConfig({ shel:'bash', oracelSelect:'impact', batch:'three',
specApproval:'no', redProof:'OFF' }) -> { ok: true }
specApproval:"no" — непустая строка truthy -> гейт подписи ВКЛЮЧЁН
redProof:"OFF" — !== 'off' -> контур red-proof ВКЛЮЧЁН
batch:"three" — Number() -> NaN -> тихий дефолт 3
shel, oracelSelect — поле проигнорировано, работает дефолт
Ни одна поддержка не воспроизведёт "у меня redProof выключен, а он работает".
tools/harness-schema.js: все поля с типами, включая вложенные background.* и
l0.*, плюс отдельный словарь СНЯТЫХ полей (judge.verify, fleet, workers) — про
них говорится "снято", а не "неизвестно", иначе владелец проекта будет чинить
то, что уже не читается.
Строгость в два шага намеренно. Значение вне типа — ошибка сразу: такой конфиг
уже сегодня ведёт себя не так, как написан. Неизвестный ключ — пока
предупреждение: у существующих проектов лежат поля от снятых спек, и отказ на
них сломал бы работающие установки на ровном месте. Отказ включается следующей
минорной версией, счётчик — SCHEMA_VERSION.
Схема писалась ПО КОДУ, а не по догадке, и это окупилось сразу: её первая
версия отвергала рабочие конфиги на branchPolicy, где выдуманное 'current'
разошлось с реальным словарём feature|none. Тест держит схему привязанной к
поставке — проверяет, что конфиг самого репозитория проходит без единого
предупреждения.
Все 33 поля задокументированы в docs/USAGE.md таблицей — до этого не был
описан ни один.
Охват 117 -> 118: tools/harness-schema.test.js.
Полный оракул на Linux: 118/118, exit 0.
Остаток D36 записан открытым: конфиг по-прежнему читают 14 независимых точек,
каждая со своим try/catch-дефолтом. Одна точка чтения — отдельный слайс, он
трогает десять файлов и не поддаётся ревью в одном диффе с введением схемы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
… 024) D28 записан с корнем: "чинить одним источником списка мест, а не ещё одной проверкой рядом". T007 добавил package.json пятым источником; шестое место — фронтматтер skills/elt/SKILL.md — оставалось несверяемым: его смотрел доктор, но не version-check, а docs/RELEASING.md писался по охвату version-check. Поэтому подъём версии строго по инструкции проходил как ok и давал четыре красных файла в полном оракуле. Место добавлено в collectVersions() — тот самый единственный список, — и docs/RELEASING.md теперь ссылается на него, а не повторяет перечисление своими словами. Инструкция следует за кодом, а не наоборот. .elt/components.json в сверку не входит намеренно: у elt/core своя версия, пришпиленная к коммиту компонента, и совпадать с версией плагина она не обязана. node tools/version-check.js: шесть источников, все 5.0.1. Полный оракул на Linux: 118/118, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
Проверка на СВЕЖЕМ клоне (тот путь, которым пойдёт чужая команда) показала дыру в T009: замок реально держит ungated-коммит, но отказ приезжал как reason "exit-4" — то есть ровно первый отказ, который встретит серверный агент, снова не давал по чему ветвиться. Дотянуты slug'и на восьми оставшихся частых местах: oracle-proof-missing, oracle-proof-stale, oracle-red (два места), task-required, certificate-missing. Регрессия утверждает не текст сообщения, а то, что reason НЕ равен generic exit-4 и принадлежит объявленному семейству — иначе следующий untagged die() снова проехал бы незамеченным. Полный оракул на Linux: 118/118, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
/code-review на полном диффе нашёл семь вещей. Четыре — регрессии, внесённые
самой 024. Записаны в docs/DEFECTS.md наравне с чужими: "сьют зелёный" и
"слайс верен" — разные утверждения, и этих четырёх сьют не покрывал.
R1. При diff.noprefix/diff.mnemonicPrefix в чужом .gitconfig treeHash
переставал видеть правки ВСЕХ отслеживаемых файлов — и печатал при этом
"дерево сверено". dropNewFileDiffs разбирал заголовок регуляркой с
обязательными a/...b/. Воспроизведено: пруф снят, файл переписан целиком,
elt gate вернул 0. Код ДО 024 здесь fail-open'ился и правку ЛОВИЛ, то есть
это регресс ровно того свойства, ради которого пруф существует.
R2. Переименование файла по-прежнему сдвигало хеш индексацией: до git add -A
это D+??, после — одна строка R old -> new плюс rename-блок в диффе. Слайс
с переносом файла был непроходим под управляемым хуком — тот самый класс,
который T003 объявил закрытым, просто не проверенный на переименовании.
Корень у R1 и R2 один и тот же, и он тот же, что у исходного дефекта: нельзя
хешировать формат, которым не управляешь. Вывод git теперь приводится к
канонической форме принудительно: -c diff.noprefix=false
-c diff.mnemonicPrefix=false --no-renames --no-ext-diff.
R3. Правило "ran === 0 при непустой выборке — не доказательство" из T004
НЕВЕРНО. Тёплый кэш даёт ровно ran:0, cached:N, total:N — замер: второй
прогон оракула печатает "118 попаданий, 0 к прогону". Второй подряд
elt commit умирал exit 4, и единственным выходом был --full, о котором
сообщение не говорило. Из пруфа тёплый кэш и подделка неотличимы В
ПРИНЦИПЕ, поэтому эвристика не могла работать. Правило снято; от подделки
защищают переезд кэша в .git/elt/ и --full у бэкстопа elt gate --ci,
который теперь игнорирует кэш всегда, а не по флагу.
R4. Поле review (читает judge-core.js:reviewConfigOf) не попало в схему T008 —
проект, который им пользуется, получал "неизвестное поле" на КАЖДОЙ
команде. Ровно тот рассинхрон, ради запрета которого схема и писалась.
R5. elt-verify-bg и project-bootstrap молча подменяли неизвестный shell на
дефолт, тогда как elt.js:sh() на том же значении отказывает: фон судил бы
конфиг, который передний план отвергает.
R6. elt init писал shell:'bash' в каждый новый проект, заново прибивая поле,
которое T001 убрал из поставки; на Windows такой конфиг умирал сразу.
D42 (унаследованный, не наш). Замок предрегистрации замера сломан коммитом
e03105b из спеки 021: runner.js правился без пересчёта хеша, и main() замера с
тех пор не стартует вовсе (exit 3). Бисекция в DEFECTS.md. Сьют не ловит — оба
теста на замок пишут свой временный preregistration.json. Правка runner.js,
внесённая T002, ОТКАЧЕНА целиком: файл побайтово равен состоянию до спеки
(925bb5cf), проба на pytest переехала в runner.test.js, который под замок не
входит. Хеш НЕ обновлён намеренно: файл объявляет immutableAfterFirstResult, а
writer-results.jsonl непуст — что делать с результатами, записанными под
незарегистрированным раннером, решает владелец замера, а не слайс.
Регрессии: две новые в elt-tree-hash.test.js (diff.noprefix, перенос файла до
и после индексации, плюс коммит слайса с переносом под хуком) и две
переписанные в elt-commit-proof.test.js — вместо снятого правила они теперь
утверждают, что тёплый кэш проходит, а gate --ci доносит ELT_ORACLE_FULL=1
через границу процесса.
Полный оракул на Linux: 118/118, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
Третья грань того же корня, найденная проверкой собственного же фикса R1/R2. core.quotepath включён в git по умолчанию, поэтому путь с кириллицей приезжает в C-кавычках: "\320\234\320\265\321\202...". Чтение содержимого такого файла с диска молча проваливалось — path.join(cwd, '"\320\234..."') не существует, а отказ съедался catch'ем рядом. Путь в хеш попадал, СОДЕРЖИМОЕ — нет. Значит правка такого файла ПОСЛЕ снятия пруфа оставалась невидимой для гейта: ровно то свойство, ради которого пруф существует. Тот же класс, что D21 (документная дверь объявляла кодом любой файл с не-ASCII именем), и лечится тем же способом: -c core.quotepath=false. Добавлен в оба вызова — status и diff, — чтобы форма была одна. Итого канонизация вывода git собрана целиком: core.quotepath=false, diff.noprefix=false, diff.mnemonicPrefix=false, --no-renames, --no-ext-diff. Один принцип: не хешировать формат, которым не управляешь. Регрессия дискриминирующая — проверено снятием флага: без него тест краснеет, с ним зелёный. Полный оракул на Linux: 118/118, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
…ws CI) Windows-джоб PR #4 упал на ОДНОМ тесте, и это моя фикстура, а не продуктовый код: `024 (ревью): elt gate --ci гоняет оракул мимо кэша` собирал команду оракула как `node -e "…"` с абсолютным путём внутри. Путь с обратными слэшами уезжает в powershell, оттуда в `node -e`, и обратный слэш перед буквой становится там "Expected unicode escape": elt oracle: node -e "require('fs').writeFileSync(\"...\Temp\...\", ...)" [eval]:1 require('fs').writeFileSync(\ ^ Expected unicode escape Пробник вынесен в файл `oracle-probe.js` и пишет по ОТНОСИТЕЛЬНОМУ пути (cwd оракула и есть корень фикстуры). Экранирования не остаётся вовсе, поэтому команда одинаково работает под bash и под powershell. Утверждение теста не ослаблено: он по-прежнему доказывает, что `gate --ci` доносит ELT_ORACLE_FULL=1 через границу процесса. Остальные 117 файлов на Windows были зелёными, включая новые регрессии на diff.noprefix и на переименование. Заодно переформулирован комментарий, который поймал замок гигиены поставки (AC3 спеки 022): пример пути в прозе — тот же абсолютный путь домашнего каталога, что замок и запрещает. Замок сработал верно. Полный оракул на Linux: 118/118, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
На windows-latest (9df6aad) упал tools/graph-journal.test.js: "writer b exited 3". Ни одной строки о причине — при том что дочерний скрипт специально пишет r.reason в stderr, а spawn шёл со stdio:'ignore'. Диагностика, которую выбрасывает тот же тест, что её производит, — не диагностика. Теперь stderr читается и попадает в сообщение об отказе. Разбор того, что известно ТОЧНО (записан как D43): - отказ по времени исключён замером: LOCK_WAIT_MS = 31 c, а 30 последовательных записей занимают 19 мс; - значит acquireLock вернул не lock-timeout, а lock-failed: <code>, то есть fs.mkdirSync бросил НЕ EEXIST, и замок счёл это фатальным (graph-journal.js:114); - на Linux не воспроизводится: тот же тест зелёный, и джоб там 50 c против 232 c на Windows. Гипотеза (НЕ подтверждена воспроизведением): на Windows одновременные rmdirSync и mkdirSync по одному пути дают транзиентные EPERM/EACCES вместо EEXIST, и лечится это ретраем тех же кодов по общему дедлайну. Продуктовый код НЕ правился: чинить единственный настоящий замок харнеса по гипотезе — ровно тот способ, которым в реестр дефектов попадает следующая строка. Следующее срабатывание теперь назовёт код ошибки, и гипотеза станет проверяемой. Значимость выше средней: graph-journal — единственный настоящий лок в дереве, и он стережёт журнал, который после cutover становится авторитетным источником состояния. Полный оракул на Linux: 118/118, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #3, which merged spec 024's T001–T007, T009 and T010. This carries the rest: T008 closes the spec, D28's root is finally closed, and — most importantly — the regressions that spec 024 itself introduced, found by the
/code-reviewrequested on #3 and reproduced end-to-end.They are recorded in
docs/DEFECTS.mdalongside everyone else's, because "the suite is green" and "the slice is correct" are different claims, and the suite covered none of them.T008 (D36) — the config is a contract
Eight of the thirty-three fields the code reads were validated. The rest did not reject a typo, they changed how the gate behaves:
specApproval: "no"is a non-empty string, so the approval gate stayed on.redProof: "OFF"is not'off', so the circuit stayed on.batch: "three"became 3. Now every field is typed, including nestedbackground.*andl0.*, with a separate dictionary of retired fields (judge.verify,fleet,workers) so they are named as retired rather than reported as typos. Types are errors; unknown keys are warnings until the next minor (SCHEMA_VERSION), because retired fields sit in existing configs. All 33 fields are now documented indocs/USAGE.md— none were before.The schema was written from the code rather than from assumption, and that paid off immediately: its first version rejected working configs on
branchPolicy, where an inventedcurrentdisagreed with the realfeature | none.D28's root
"Version declared in six places,
version-checkchecks four" was recorded with the root cause "fix it with one source for the list of places, not another check beside it." The sixth place —skills/elt/SKILL.mdfrontmatter — was checked by the doctor but not byversion-check, anddocs/RELEASING.mdwas written toversion-check's coverage. So bumping the version strictly by the instructions passed asokand produced four red files in the full oracle. The place was added tocollectVersions()— that single list — andRELEASING.mdnow points at it instead of restating it.The regressions spec 024 introduced
All three of the first group share one root, and it is the same root as the original defect: do not hash a format you do not control.
treeHashwent blind underdiff.noprefix. The diff header was parsed with a regex requiring literala/…b/. Withdiff.noprefixordiff.mnemonicPrefixin someone's.gitconfig, no block matched and the entire diff dropped out of the hash — while the gate printed "дерево сверено" and exited 0. Reproduced: proof taken, tracked file rewritten in full, gate accepted. The pre-024 code failed open here and caught the edit, so this was a regression of exactly the property the proof exists for.git add -Aa move isD old+?? new; after, it is oneR old -> newrow plus a rename block in the diff. A slice containing a file move could not be committed under the managed hook — the very defect class T003 declared closed, simply never tested on a rename.core.quotepathis on by default, so such a path arrives C-quoted ("\320\234…"); reading the file from disk then failed and was swallowed by the adjacentcatch. The path made it into the hash, the contents did not — so editing such a file after the proof stayed invisible to the gate. Same class as D21. The regression is discriminating: verified red with the flag removed.Git's output is now forced canonical in one place:
-c core.quotepath=false -c diff.noprefix=false -c diff.mnemonicPrefix=false --no-renames --no-ext-diff.ran === 0rule was simply wrong. A warm cache produces exactlyran: 0, cached: N, total: N; measured on this repo, a second oracle run prints "118 попаданий, 0 к прогону". So the secondelt commitin a row died with exit 4, and the only escape was--full, which the message never mentioned. From the proof alone a warm cache and a forged one are indistinguishable in principle, so the heuristic could never have worked. It is removed; the real protection against E17 is the cache's move into.git/elt/pluselt gate --cinow running with--fullalways rather than on a flag — the CI backstop does not trust the cache.review(read byjudge-core.js:reviewConfigOf) was missing from the schema, so a project using it got "неизвестное поле" on every command — the same read/schema drift the schema was written to forbid.elt-verify-bgandproject-bootstrapsilently substituted a default for an unknownshellwhile the foreground refuses it, so the background would judge a config the foreground rejects. Andelt initwroteshell: "bash"into every new project, re-pinning the field T001 had just removed from the shipped config — on Windows that config dies immediately.D42 — inherited, not ours, and it needs your decision
The benchmark's preregistration lock (
verifyRunnerHash) has been broken sincee03105b(spec 021), whererunner.jswas edited without recomputingpreregistration.json.runner.sha256. Bisection is indocs/DEFECTS.md:main()refuses to start with exit 3, so the whole benchmark contour has been unusable since 021. The suite cannot catch it: both lock tests write their own temporary preregistration, so they exercise the mechanism and never the shipped files.Spec 024's own edit to
runner.js(the pytest preflight from T002) is reverted in full — the file is byte-identical to its pre-024 state (925bb5cf…) — and the probe moved intorunner.test.js, which is not under the lock: it does not take part in the measurement, it checks it.The hash is deliberately not updated. The file declares
immutableAfterFirstResult: trueandwriter-results.jsonlis non-empty, so editing those fields is forbidden by the artifact itself, and what to do about results recorded under an unregistered runner is the measurement owner's call, not a slice's. A lock test comparing the shipped preregistration against the shipped runner is likewise not added: it would be red today, and a red test in the suite is a closed door for every other slice. Both wait on your decision.Test plan
Measured on Linux, Node 22.22.2, clean tree, after rebasing onto the merged
main.Regressions in this branch:
tools/elt-tree-hash.test.js— three new cases: a tracked-file rewrite stays visible underdiff.noprefix; a rename is refused identically before and aftergit add -A, and a slice containing a move commits cleanly under the managed hook; a Cyrillic-named file's edit is seen (verified discriminating by removing the flag).tools/elt-commit-proof.test.js— the two tests written for the wrongran === 0rule are replaced by tests of the correct behaviour: a warm cache passes the gate, andelt gate --ciis proven to carryELT_ORACLE_FULL=1across the process boundary (the oracle is a shell string, not argv).tools/harness-schema.test.js— the exact config that used to validate is rejected; retired fields are named as retired; the repo's own config passes with zero warnings; and a coverage test lists every field the code reads,reviewincluded.tools/version-check.test.js— six sources, and each one alone can break the check.benchmarks/gemini-3.7-flash-high/runner.test.js— an honest skip naming the missing dependency, with the probe living in the test rather than in the locked runner.full suite exits 0
instruction drift check exits 0
the smallest regression for this branch exists (or the change is trivial)
🤖 Generated with Claude Code
https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF