Skip to content
This repository was archived by the owner on Oct 6, 2026. It is now read-only.

fix: spec 024 tail — config contract, D28 root, and four of its own regressions - #4

Merged
prodelt merged 7 commits into
mainfrom
claude/harness-analysis-optimization-z5xtwm
Sep 4, 2026
Merged

prodelt merged 7 commits into
mainfrom
claude/harness-analysis-optimization-z5xtwm

Conversation

@prodelt

@prodelt prodelt commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

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-review requested on #3 and reproduced end-to-end.

They are recorded in docs/DEFECTS.md alongside 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:

validateHarnessConfig({ shel: 'bash', oracelSelect: 'impact', batch: 'three',
                        specApproval: 'no', redProof: 'OFF' })   // → { ok: true }

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 nested background.* and l0.*, 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 in docs/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 invented current disagreed with the real feature | none.

D28's root

"Version declared in six places, version-check checks 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.md frontmatter — was checked by the doctor but not by version-check, and docs/RELEASING.md was written to version-check's coverage. So bumping the version strictly by the instructions passed as ok and produced four red files in the full oracle. The place was added to collectVersions() — that single list — and RELEASING.md now 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.

  • R1 — treeHash went blind under diff.noprefix. The diff header was parsed with a regex requiring literal a/…b/. With diff.noprefix or diff.mnemonicPrefix in 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.
  • R2 — renames still shifted the hash across staging. Before git add -A a move is D old + ?? new; after, it is one R old -> new row 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.
  • R3b — a non-ASCII filename's content never entered the hash. Found by adversarially re-reading my own fix. core.quotepath is on by default, so such a path arrives C-quoted ("\320\234…"); reading the file from disk then failed and was swallowed by the adjacent catch. 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.

  • R3 — the ran === 0 rule was simply wrong. A warm cache produces exactly ran: 0, cached: N, total: N; measured on this repo, a second oracle run prints "118 попаданий, 0 к прогону". So the second elt commit in 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/ plus elt gate --ci now running with --full always rather than on a flag — the CI backstop does not trust the cache.
  • R4–R6. review (read by judge-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-bg and project-bootstrap silently substituted a default for an unknown shell while the foreground refuses it, so the background would judge a config the foreground rejects. And elt init wrote shell: "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 since e03105b (spec 021), where runner.js was edited without recomputing preregistration.json.runner.sha256. Bisection is in docs/DEFECTS.md:

OK        00138e0  actual=a65f583b7106  expected=a65f583b7106
MISMATCH  e03105b  actual=925bb5cfb4b8  expected=a65f583b7106   ← broke here

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 into runner.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: true and writer-results.jsonl is 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.

node tools/elt-oracle-runner.js --full   # 118/118 — exit 0
node tools/gen-agents-md.js --check      # копии совпадают с CLAUDE.md — exit 0
node bin/doctor.js                       # exit 0
node tools/version-check.js              # ok — 5.0.1 (six sources)

Regressions in this branch:

  • tools/elt-tree-hash.test.js — three new cases: a tracked-file rewrite stays visible under diff.noprefix; a rename is refused identically before and after git 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 wrong ran === 0 rule are replaced by tests of the correct behaviour: a warm cache passes the gate, and elt gate --ci is proven to carry ELT_ORACLE_FULL=1 across 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, review included.

  • 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

prodelt and others added 7 commits September 4, 2026 06:46
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
@prodelt
prodelt merged commit 9fb2cde into main Sep 4, 2026
4 checks passed
@prodelt
prodelt deleted the claude/harness-analysis-optimization-z5xtwm branch October 6, 2026 10:41
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant