Skip to content

Proof: a dispatch refuses evidence its lanes cannot download (V5b2d-4c) - #548

Merged
lemone112 merged 8 commits into
mainfrom
v5b2d-4c-dispatch-binds-real-evidence
Aug 7, 2026
Merged

Proof: a dispatch refuses evidence its lanes cannot download (V5b2d-4c)#548
lemone112 merged 8 commits into
mainfrom
v5b2d-4c-dispatch-binds-real-evidence

Conversation

@lemone112

@lemone112 lemone112 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Наблюдаемый дефект

Координатор проверял, что --evidence-run-id — положительное целое, но не то, что такой прогон существует и несёт названный артефакт. Положительность не является свидетельством существования.

Я запустил диспатч против run 99999999 и получил 133 обречённых прогона, каждый падал за 9–11 секунд на скачивании несуществующего артефакта. Одна ошибка координатора превращается в 256 обречённых задач, и узнаёшь об этом 256 раз подряд вместо одного.

Закон

Перед первым диспатчем координатор спрашивает, несёт ли названный прогон названный артефакт, и отказывает закрыто (exit 64).

Наблюдение GitHub — единственная нечистая граница — инъектируется, поэтому само решение проверяется без сети. Это держит границу «pure core / imperative shell», которую требует AGENTS.md.

Любой сбой наблюдения — нет прогона, нет токена, битый ответ — тоже отказ, а не падение и не молчаливый проход. --dry-run остаётся офлайновым по контракту, гейт живёт только на боевом пути.

Доказательства

Четыре RED-теста: прогон без нужного артефакта; недостижимый прогон (типизированный отказ, не исключение); успешный допуск; anti-vacuity — наблюдателя спрашивают именно про названный прогон, иначе диспатч мог бы связать один прогон, а артефакты прочитать у другого.

Проверка реальностью, а не только фикстурами:

admit_evidence_artifact_v1(99999999,    'verification-evidence-arb')  -> ОТКАЗ
admit_evidence_artifact_v1(31089986150, 'verification-evidence-arb')  -> ДОПУЩЕН
admit_evidence_artifact_v1(31089986150, 'verification-evidence-mpfi') -> ДОПУЩЕН

Первая строка — ровно тот вход, что стоил 133 прогонов.

Границы среза

Затрагивает только corpus_dispatch.py и его тест — не пересекается по файлам с PR #547 (протокол, квитанции, полосы), поэтому идёт параллельной полосой в отдельном worktree, как того требует роадмап.

Rollback

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

Поправка к более раннему утверждению этого PR

Я писал, что «поведение --dry-run не менялось». Это неверно — нашла линза норм при пере-ревью. На main вызов verification-dispatch с чужим именем артефакта и --dry-run даёт нетипизированный TypeError: 'ShardCorpusRejectedV1' object is not iterable; на ветке он даёт exit 64. Поведение изменилось, в лучшую сторону, но заявление было ложным.

Summary by CodeRabbit

  • Новые возможности

    • Безопасный режим печати команд теперь используется по умолчанию; выполнение требует --execute и явного --expect-lanes.
    • Добавлена проверка требуемого артефакта GitHub Actions перед отправкой verification-команд, включая проверку доступности и срока действия.
    • Добавлена постраничная обработка списка артефактов.
  • Исправления

    • Ошибки наблюдения и отсутствующие артефакты приводят к типизированному отказу без аварийного завершения.
    • Некорректные команды и ошибки отдельных направлений завершаются с кодом 64 и отчётом о прогрессе.
    • Режимы печати и планирования не выполняют сетевые обращения.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eb1ff8b3-abdb-416b-9929-12647f9edcf0

📥 Commits

Reviewing files that changed from the base of the PR and between ebb08dd and 0308622.

📒 Files selected for processing (2)
  • proof/region/v1/corpus_dispatch.py
  • proof/region/v1/tests/test_corpus_dispatch.py

Walkthrough

CLI получает список артефактов указанного GitHub Actions run, проверяет evidence перед dispatch и требует явное выполнение с подтверждённым числом lane. Режим печати не выполняет сеть. Ошибки возвращают код 64.

Changes

Допуск evidence и dispatch

Layer / File(s) Summary
Наблюдение и допуск evidence-артефакта
proof/region/v1/corpus_dispatch.py, proof/region/v1/tests/test_corpus_dispatch.py
Добавлены постраничное получение артефактов через gh api, строгий разбор wire-формата и admit_evidence_artifact_v1. Ошибки наблюдения, отсутствие и истечение артефакта приводят к типизированному отказу.
Контракт CLI и локальная валидация
proof/region/v1/corpus_dispatch.py, proof/region/v1/tests/test_corpus_dispatch.py, proof/region/v1/tests/test_verification_dispatch.py
Режим печати стал режимом по умолчанию. --dry-run удалён. Реальное выполнение требует --execute и обязательного --expect-lanes. Локальные ошибки отклоняют dispatch до сетевого наблюдения.
Запуск lane-команд и обработка отказов
proof/region/v1/corpus_dispatch.py, proof/region/v1/tests/test_corpus_dispatch.py
После допуска запускаются lane-команды. Ошибки OSError, CalledProcessError и частичный dispatch возвращают код 64 и отчёт о прогрессе.

Estimated code review effort: 4 (Сложный) | ~45 минут

Sequence Diagram(s)

sequenceDiagram
  participant CLI as main
  participant Admission as admit_evidence_artifact_v1
  participant GHAPI as gh api
  participant Dispatch as gh workflow run
  CLI->>Admission: проверка evidence run
  Admission->>GHAPI: получение артефактов
  GHAPI-->>Admission: список артефактов или ошибка
  Admission-->>CLI: допуск или отказ
  alt evidence допущен
    CLI->>Dispatch: запуск lane-команд
    Dispatch-->>CLI: результат или ошибка
  else evidence отклонён
    CLI-->>CLI: возврат кода 64
  end
Loading

Possibly related PRs

  • Labpics-Team/lab-colors#529: PR связан с workflow assemble-lanes, который потребляет lane-артефакты и run ID.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Заголовок точно описывает основное изменение: dispatch отклоняет evidence, если его lane не могут скачать.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v5b2d-4c-dispatch-binds-real-evidence

Comment @coderabbitai help to get the list of available commands.

Claude Code added 3 commits August 6, 2026 18:48
Наблюдаемый дефект: coordinator проверял, что --evidence-run-id положительное
целое, но не то, что такой прогон существует и несёт названный артефакт.
Положительность — не свидетельство существования. Я запустил диспатч против
run 99999999 и получил 133 обречённых прогона, каждый падал за 9-11 секунд на
скачивании несуществующего артефакта.

Одна ошибка координатора превращается в 256 обречённых задач, и узнаёшь ты об
этом 256 раз подряд вместо одного.

Закон: перед первым диспатчем координатор спрашивает, несёт ли названный
прогон названный артефакт, и отказывает закрыто. Наблюдение GitHub — единственная
нечистая граница — инъектируется, поэтому само решение проверяется без сети.
Любой сбой наблюдения (нет прогона, нет токена, битый ответ) — тоже отказ, а не
падение и не молчаливый проход. Dry run остаётся офлайновым по контракту, гейт
живёт только на боевом пути.

Доказательства: четыре RED-теста (прогон без артефакта, недостижимый прогон,
успешный допуск, anti-vacuity — наблюдателя спрашивают именно про названный
прогон) плюс проверка реальностью: admit_evidence_artifact_v1(99999999, ...)
отвергает тот самый вход, что стоил 133 прогонов, а 31089986150 допускается для
обоих движков.

Локально: 14 тестов координатора, OK.
…n provably

Две независимые линзы ревью дали FAIL. Находки закрыты; две из них означали,
что гейт лишь ВЫГЛЯДЕЛ рабочим.

1. Истёкшие артефакты. Листинг GitHub включает артефакты с expired:true, скачать
   которые нельзя. Гейт брал имя не глядя, поэтому старый evidence-run проходил
   бы допуск и снова давал 256 обречённых полос — ровно тот класс, который срез
   объявляет закрытым, только через соседний путь. Теперь jq отбирает
   `select(.expired | not)`.

2. Шов не был покрыт. Четыре теста доказывали РЕШЕНИЕ допуска, но ни один не
   доказывал, что main() спрашивает. Удаление прошивки оставляло все 14 тестов
   зелёными. Добавлены четыре теста шва: отказ не диспатчит ничего, допуск
   диспатчит все 256 полос, dry run никого не спрашивает и ничего не запускает,
   отвергнутый набор команд уходит через exit 64. Саботаж подтверждает
   чувствительность: удаление прошивки красит test_a_refused_evidence_run_
   dispatches_nothing.

3. Точка инъекции была декларативной. Значение по умолчанию связывало
   gh_run_artifact_names_v1 при определении функции, поэтому подмена наблюдателя
   не действовала: шов выглядел инъектируемым и не был им. Дефект вскрыл сам
   тест шва — резолвинг перенесён в тело.

4. Отвергнутый набор команд попадал в цикл и давал нетипизированный
   TypeError: 'ShardCorpusRejectedV1' object is not iterable. Дефект
   предсуществующий, но мой guard `type(commands) is tuple` был написан ВОКРУГ
   отказа, не обработав его, и нарушал тот самый инвариант, который срез
   заявляет. Теперь типизированный exit 64.

5. Пагинация: gh api без --paginate отдаёт одну страницу; имя на второй странице
   читалось бы как отсутствующее. Добавлен --paginate. Направление и без того
   было fail-closed, но ложный отказ на валидном прогоне — тоже дефект.

6. Причина отказа теперь едет с отказом: оператор обязан отличать «нет такого
   прогона» от «нет токена», иначе диагностика инцидента требует ручного
   повтора вызова.

Проверка реальностью: 99999999 отвергается, 31089986150 и 31022375756 (оба
движка, живые артефакты) допускаются.

Локально: 18 тестов координатора, OK.
…rule where a test can reach it

Пере-ревью финального состояния. Два дефекта, оба доказаны мутацией, и оба
означали, что защита выглядит рабочей и не является ею.

1. Тесты шва не закрепляли КООРДИНАТУ RUN_ID. Оба подменённых наблюдателя
   игнорировали аргумент, поэтому мутант, зашивающий чужой id в вызов допуска
   (admit_evidence_artifact_v1(1, args.evidence_artifact)), выживал при всех 18
   зелёных тестах. Координата артефакта была закреплена, а run_id — тот самый,
   на котором и произошёл инцидент с 99999999, — нет. Живой сценарий: гейт
   наблюдает прогон A и допускает, а 256 полос диспатчатся связанными с
   прогоном B. Наблюдатели шва сделаны чувствительными к run_id; мутант гибнет.

2. Правило истёкших артефактов жило в jq-выражении, то есть за сетью, и не было
   покрыто ничем: удаление select(.expired | not) оставляло все 18 тестов
   зелёными. Наблюдение теперь только НАБЛЮДАЕТ и возвращает пары (имя,
   expired), а решение, что считается свидетельством, применяется в чистом
   ядре, где до него дотягивается тест. Два теста: истёкший артефакт — отказ;
   живой рядом с истёкшим — допуск (иначе правило выродилось бы в сплошной
   отказ). Мутант «убрать фильтр» гибнет.

3. Причина отказа несла repr(error), а CalledProcessError.__repr__ отбрасывает
   stderr — ровно тот текст, который отличает «нет такого прогона» от «нет
   токена». Комментарий обещал различимость, код её не давал. Теперь detail
   несёт stderr: живая проверка даёт «cannot observe run 99999999: gh: Not
   Found (HTTP 404)».

Проверка реальностью: 99999999 отвергается с причиной, 31089986150 допускается,
наблюдение возвращает обе пары с expired=False.

Локально: 20 тестов координатора, OK.
@lemone112
lemone112 force-pushed the v5b2d-4c-dispatch-binds-real-evidence branch from e04950c to 546ff04 Compare August 6, 2026 15:49
…ly claimed

Two independent reviews of the final state — one of them running actual
mutants — showed this slice narrowed the incident class without closing it,
and that its own commit message overstated what was pinned.

THE POLARITY.  The incident was a forgotten `--dry-run`, and the fix left the
dangerous run as the default: safety still meant remembering a flag.  Printing
is now what happens, `--execute` is the opt-in, and it has to name the
campaign's size with `--expect-lanes`.  Swapping the two widths is a realistic
slip — `--lane-width 256 --shard-width 4` is 65536 runs — and the only cheap
moment to notice is before the first invocation.

THE ARTIFACT COORDINATE.  A mutant hardcoding `EVIDENCE_ARTIFACTS_V1[0]` at
the admission call site survived all twenty tests: both seam tests always
passed the arb name, so pinning one engine was indistinguishable.  The
producer runs its two engines as independent jobs, so a run really can carry
one artifact and not the other, and admitting the wrong one buys 256 doomed
lanes — the incident down a second axis.  The previous message claimed this
coordinate was pinned; it was pinned where commands are built, not where the
admission asks.

THE EXPIRY RULE.  Moving the filter out of the query put the decision where a
test could reach it and left the decoding that produces it behind the network.
Two mutants survived: one that never marks anything expired, one that drops
the field from the query.  `parse_artifact_listing_v1` is pure now and refuses
a listing that is not the two-column form it asked for — the old parser read
an unrecognised line as "live", which admits exactly the stale evidence run
this gate exists to refuse.  The repository currently holds 127 expired
artifacts, so the path is not hypothetical.

A campaign that dies mid-flight now reports how many lanes it launched and
returns the typed exit instead of a traceback, so the retry can resume rather
than duplicate.

Every repair is proven by the mutant it kills, each failing exactly one test:
the hardcoded artifact, the never-expired listing, the field dropped from the
query, and `repr(error)` in place of the stderr that tells "no such run" from
"no token".

Verified: 276 local tests, 7 errors — the exact Windows baseline.

Left out deliberately, and not hidden: the admission still checks that the
named artifact exists, not that it carries `job.bin` and the comparator
bundle, and not which workflow or commit produced it.  Those are a different
class — supply-chain provenance rather than operator error — and want their
own slice.
@lemone112

Copy link
Copy Markdown
Collaborator Author

Два независимых ревью финального состояния (одно — с реальными мутантами) показали, что срез сужал класс инцидента, но не закрывал его, и что мой текст коммита переоценил закреплённое.

Исправляю собственное ложное утверждение. Я писал «координата артефакта была закреплена, а run_id — нет». Для шва допуска это неверно: артефакт был закреплён там, где строятся команды, а не там, где допуск спрашивает. Мутант с EVIDENCE_ARTIFACTS_V1[0] на месте вызова выживал на всех 20 тестах, потому что оба seam-теста всегда подавали arb-имя.

Что исправлено в e45b746:

  1. Полярность флага. Инцидент был забытым --dry-run, а срез оставил опасный запуск дефолтом — безопасность по-прежнему означала «помнить флаг». Теперь печать это то, что происходит по умолчанию, живой запуск — явный --execute, и он обязан назвать масштаб через --expect-lanes. Перепутанные местами ширины (--lane-width 256 --shard-width 4) дают 65 536 прогонов, и единственный дешёвый момент это заметить — до первого вызова.
  2. Координата артефакта на шве допуска закреплена тестом. Продюсер запускает движки независимыми job'ами, поэтому прогон реально может нести один артефакт и не нести другой.
  3. Правило истечения. Разбор expired вынесен в чистую parse_artifact_listing_v1 и закрыт наглухо: прежний парсер читал неопознанную строку как «живой артефакт», то есть допускал ровно тот устаревший прогон, ради отказа которому гейт и существует. В репозитории сейчас 127 истёкших артефактов — путь не гипотетический.
  4. Частичный сбой кампании даёт число запущенных полос и типизированный выход вместо трейсбека, чтобы повтор возобновлял, а не дублировал.

Каждое исправление доказано убитым мутантом, каждый роняет ровно один тест: захардкоженный артефакт, «ничего не истекло», поле убрано из запроса, repr(error) вместо stderr.

Намеренно не сделано и не спрятано: допуск проверяет наличие названного артефакта, но не его содержимое (job.bin, comparator-bundle) и не происхождение прогона (workflow, коммит, событие). Это другой класс — провенанс цепочки поставки, а не операторская ошибка — и заслуживает отдельного среза.

Проверки: 276 локальных тестов, 7 ошибок — ровно известная база Windows (слой arb/mpfi не импортируется без fcntl).

… it was pinned

A third review ran fifteen mutants against the previous commit; nine survived,
and all but one lived in the code that commit added.  The polarity change and
the partial-failure report were shipped without a single test that could hold
them.

THE RUN ID.  All three seam tests take the run id from one helper that
hardcodes 424242, so a call site hardcoding that same number was
indistinguishable from one that reads the argument — while the test's own
comment claimed the opposite in as many words.  The mirror axis was fixed last
round and this one left standing.  A seam test now names a different run and
records what the observer was actually asked.

THE DECLARED SCALE.  Making `--expect-lanes` optional survived every test,
because falling through to the mismatch comparison refuses too — with a
message that tells the operator nothing.  It has its own refusal now, and the
test reads the message rather than only the exit code: the two differ exactly
where the operator does.

THE PARTIAL FAILURE.  Deleting the whole handler, returning 0 mid-campaign, or
moving the counter before the call all stayed green.  The count is the point —
a retry that cannot tell three from zero duplicates everything already
dispatched — so the test now reads "after 3 of 256" out of the report.  The
handler also catches OSError: a missing `gh` abandoned a campaign with a
traceback and no count.

ORDER.  The authenticated observation ran before the free local check, so a
mistyped width spent an API call and answered with a diagnosis about the
evidence run.  The cheap refusals come first now, which is what the comment
claimed all along.

`--mode plan --execute` was a silent no-op: a flag with live semantics that is
quietly ignored is worse than one that refuses, because the operator believes
a campaign started.

`--paginate` was checked by reading the function's source, which a mutant that
drops the flag from argv and leaves the word in a comment passes untouched.
The check is behavioural now — it asserts the argv actually built, plus
`check` and `capture_output`, which the source read never saw.

Names that referred to the removed flag are gone; only the comment recounting
the incident still says `--dry-run`, which is history and stays.

Every repair is proven by the mutant it kills, each failing exactly one test:
the hardcoded run id, the optional scale, the vanished counter, `return 0`
mid-campaign, the ignored `--execute` in plan mode, and `--paginate` demoted
to a comment.

Verified: 282 local tests, 7 errors — the exact Windows baseline.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proof/region/v1/corpus_dispatch.py`:
- Around line 202-215: Добавьте ограничение времени выполнения в вызов
subprocess.run внутри функции, выполняющей запрос списка артефактов через gh
api. Используйте существующую конфигурацию таймаута, если она доступна; иначе
задайте явный конечный timeout, чтобы зависший процесс завершался исключением,
которое обработает admit_evidence_artifact_v1.
- Around line 409-433: Объедините обработчики OSError и
subprocess.CalledProcessError в один общий except с тем же поведением: сохранить
сообщение dispatch stopped и возврат 64. Перенесите оба поясняющих комментария
над объединённым обработчиком, не изменяя логику цикла запуска команд.

In `@proof/region/v1/tests/test_corpus_dispatch.py`:
- Around line 357-365: В заглушке record сохраните переданные kwargs в доступной
после вызова структуре вместо проверки через assert; затем после вызова
проверьте значения options[0].get("check") и options[0].get("capture_output") с
помощью self.assertIs(..., True), чтобы проверки выполнялись и при python -O.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2d9d3584-6c2a-4027-9a7d-1f4339cfb3d6

📥 Commits

Reviewing files that changed from the base of the PR and between e420e21 and ebb08dd.

📒 Files selected for processing (3)
  • proof/region/v1/corpus_dispatch.py
  • proof/region/v1/tests/test_corpus_dispatch.py
  • proof/region/v1/tests/test_verification_dispatch.py

Comment thread proof/region/v1/corpus_dispatch.py
Comment thread proof/region/v1/corpus_dispatch.py
Comment thread proof/region/v1/tests/test_corpus_dispatch.py
@lemone112 lemone112 closed this Aug 6, 2026
@lemone112 lemone112 reopened this Aug 6, 2026
lemone112 pushed a commit that referenced this pull request Aug 7, 2026
Final-state review anchored on the commit object — after catching its own
first mutation pass reading a stale file through WSL — ran 26 mutants and
found two blockers plus one theatre test.

THE CRASH.  A refused command set is not iterable, and `main` fell straight
into the loop with it: a mistyped artifact name — the likeliest operator slip
— crashed with a TypeError while this very commit's docstring promises never
a crash.  The same fallthrough was fixed on the #548 branch, but this branch
grows from main, where it still lived.  Typed exit now, and the CLI test
covers three foreign coordinates while also proving the expensive download
never ran for any of them — a coordinate the pure builder already refused
must not cost a network call.

THE FALSE CLAIM.  The comment said `CalledProcessError.__repr__` drops
stderr.  Measured: it does not — `BaseException` keeps every constructor
argument in `args`.  The preference for `stderr` over `repr` is legibility,
not recovery, and the comment now says so instead of inventing a stronger
reason.

THE THEATRE.  The foreign-entries test fed bytes, None and an int to the type
guard — inputs that can never equal a string, so the guard was semantically
free and the test held without the code it pointed at.  An unhashable list is
what makes the guard load-bearing: without it the observation raises instead
of refusing.  The test feeds one now, and stripping the guard fails it.

Also: the download carries a deadline.  A hung `gh` stalled the coordinator
forever on the last check standing between an operator and 256 dispatches,
indistinguishable from work in progress.

Each repair proven by its mutant: the restored fallthrough reddens the new
CLI test, the stripped guard reddens the strengthened one.

Verified: 31 dispatch tests pass; 426 on Linux with the three pre-existing
`test_build` failures that also fail on an untouched tree.
CodeRabbit's remark stands: a hung `gh` stalled the coordinator forever on
the one call between an operator and 256 dispatches, indistinguishable from
work in progress.  The listing now times out, the admission already turns
every observation failure into a typed refusal, and the wire test pins the
deadline the same way it pins `check` and `capture_output`.

Proven by the mutant: removing the timeout reddens exactly that test.
Verified: 34 dispatch tests pass.
@lemone112

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Замечание про таймаут подтверждено и закрыто в f7d0875: зависший gh подвешивал координатор навсегда на единственном вызове между оператором и 256 диспатчами. Листинг теперь ограничен OBSERVATION_TIMEOUT_SECONDS_V1, срабатывание таймаута уходит в типизированный отказ через существующий except, и wire-тест закрепляет дедлайн так же, как check и capture_output. Доказано мутантом: снятие таймаута роняет ровно этот тест.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@lemone112 Подтверждено. Ограничение OBSERVATION_TIMEOUT_SECONDS_V1 предотвращает бессрочное ожидание gh перед dispatch. Typed rejection сохраняет безопасный отказ при таймауте. Wire-тест также закрепляет передачу timeout.


✏️ Learnings added
Learnt from: lemone112
URL: https://github.com/Labpics-Team/lab-colors/pull/548

Timestamp: 2026-08-07T06:03:42.643Z
Learning: In `proof/region/v1/corpus_dispatch.py`, the GitHub Actions artifact-listing observation must pass `timeout=OBSERVATION_TIMEOUT_SECONDS_V1` to the `gh` subprocess call. A timeout must follow the existing typed rejection path so that artifact observation cannot block live corpus dispatch indefinitely. The wire test verifies `timeout` together with `check` and `capture_output`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

…check missed

Both CodeRabbit threads were right and are closed in code rather than argued.
The two exception handlers had byte-identical bodies split only to carry two
comments; one handler now catches both and one comment covers every way a
campaign dies mid-flight.  The stub's asserts would vanish under
PYTHONOPTIMIZE=2 — and the CI worker runs this suite under exactly that — so
the kwargs are recorded and asserted outside the stub, where the optimizer
cannot delete them.

The merge gate's surviving mutant is dead too: weakening the scale check from
`!=` to `<` passed every test, meaning an operator declaring 512 lanes over a
256-lane plan would dispatch silently.  A declaration that does not match
reality is a wrong mental model regardless of its sign, and the new test pins
the direction nothing else looked at.

Verified: 35 dispatch tests pass, and pass again under PYTHONOPTIMIZE=2; the
`<` mutant reddens exactly the new test.
@lemone112
lemone112 merged commit 5739e16 into main Aug 7, 2026
10 checks passed
@lemone112
lemone112 deleted the v5b2d-4c-dispatch-binds-real-evidence branch August 7, 2026 07:12
lemone112 added a commit that referenced this pull request Aug 7, 2026
…me (#552)

* Proof: a dispatch refuses evidence its lanes cannot download (V5b2d-4c)

Наблюдаемый дефект: coordinator проверял, что --evidence-run-id положительное
целое, но не то, что такой прогон существует и несёт названный артефакт.
Положительность — не свидетельство существования. Я запустил диспатч против
run 99999999 и получил 133 обречённых прогона, каждый падал за 9-11 секунд на
скачивании несуществующего артефакта.

Одна ошибка координатора превращается в 256 обречённых задач, и узнаёшь ты об
этом 256 раз подряд вместо одного.

Закон: перед первым диспатчем координатор спрашивает, несёт ли названный
прогон названный артефакт, и отказывает закрыто. Наблюдение GitHub — единственная
нечистая граница — инъектируется, поэтому само решение проверяется без сети.
Любой сбой наблюдения (нет прогона, нет токена, битый ответ) — тоже отказ, а не
падение и не молчаливый проход. Dry run остаётся офлайновым по контракту, гейт
живёт только на боевом пути.

Доказательства: четыре RED-теста (прогон без артефакта, недостижимый прогон,
успешный допуск, anti-vacuity — наблюдателя спрашивают именно про названный
прогон) плюс проверка реальностью: admit_evidence_artifact_v1(99999999, ...)
отвергает тот самый вход, что стоил 133 прогонов, а 31089986150 допускается для
обоих движков.

Локально: 14 тестов координатора, OK.

* Proof: the gate must refuse what cannot be downloaded, and be wired in provably

Две независимые линзы ревью дали FAIL. Находки закрыты; две из них означали,
что гейт лишь ВЫГЛЯДЕЛ рабочим.

1. Истёкшие артефакты. Листинг GitHub включает артефакты с expired:true, скачать
   которые нельзя. Гейт брал имя не глядя, поэтому старый evidence-run проходил
   бы допуск и снова давал 256 обречённых полос — ровно тот класс, который срез
   объявляет закрытым, только через соседний путь. Теперь jq отбирает
   `select(.expired | not)`.

2. Шов не был покрыт. Четыре теста доказывали РЕШЕНИЕ допуска, но ни один не
   доказывал, что main() спрашивает. Удаление прошивки оставляло все 14 тестов
   зелёными. Добавлены четыре теста шва: отказ не диспатчит ничего, допуск
   диспатчит все 256 полос, dry run никого не спрашивает и ничего не запускает,
   отвергнутый набор команд уходит через exit 64. Саботаж подтверждает
   чувствительность: удаление прошивки красит test_a_refused_evidence_run_
   dispatches_nothing.

3. Точка инъекции была декларативной. Значение по умолчанию связывало
   gh_run_artifact_names_v1 при определении функции, поэтому подмена наблюдателя
   не действовала: шов выглядел инъектируемым и не был им. Дефект вскрыл сам
   тест шва — резолвинг перенесён в тело.

4. Отвергнутый набор команд попадал в цикл и давал нетипизированный
   TypeError: 'ShardCorpusRejectedV1' object is not iterable. Дефект
   предсуществующий, но мой guard `type(commands) is tuple` был написан ВОКРУГ
   отказа, не обработав его, и нарушал тот самый инвариант, который срез
   заявляет. Теперь типизированный exit 64.

5. Пагинация: gh api без --paginate отдаёт одну страницу; имя на второй странице
   читалось бы как отсутствующее. Добавлен --paginate. Направление и без того
   было fail-closed, но ложный отказ на валидном прогоне — тоже дефект.

6. Причина отказа теперь едет с отказом: оператор обязан отличать «нет такого
   прогона» от «нет токена», иначе диагностика инцидента требует ручного
   повтора вызова.

Проверка реальностью: 99999999 отвергается, 31089986150 и 31022375756 (оба
движка, живые артефакты) допускаются.

Локально: 18 тестов координатора, OK.

* Proof: pin the coordinate the incident turned on, and put the expiry rule where a test can reach it

Пере-ревью финального состояния. Два дефекта, оба доказаны мутацией, и оба
означали, что защита выглядит рабочей и не является ею.

1. Тесты шва не закрепляли КООРДИНАТУ RUN_ID. Оба подменённых наблюдателя
   игнорировали аргумент, поэтому мутант, зашивающий чужой id в вызов допуска
   (admit_evidence_artifact_v1(1, args.evidence_artifact)), выживал при всех 18
   зелёных тестах. Координата артефакта была закреплена, а run_id — тот самый,
   на котором и произошёл инцидент с 99999999, — нет. Живой сценарий: гейт
   наблюдает прогон A и допускает, а 256 полос диспатчатся связанными с
   прогоном B. Наблюдатели шва сделаны чувствительными к run_id; мутант гибнет.

2. Правило истёкших артефактов жило в jq-выражении, то есть за сетью, и не было
   покрыто ничем: удаление select(.expired | not) оставляло все 18 тестов
   зелёными. Наблюдение теперь только НАБЛЮДАЕТ и возвращает пары (имя,
   expired), а решение, что считается свидетельством, применяется в чистом
   ядре, где до него дотягивается тест. Два теста: истёкший артефакт — отказ;
   живой рядом с истёкшим — допуск (иначе правило выродилось бы в сплошной
   отказ). Мутант «убрать фильтр» гибнет.

3. Причина отказа несла repr(error), а CalledProcessError.__repr__ отбрасывает
   stderr — ровно тот текст, который отличает «нет такого прогона» от «нет
   токена». Комментарий обещал различимость, код её не давал. Теперь detail
   несёт stderr: живая проверка даёт «cannot observe run 99999999: gh: Not
   Found (HTTP 404)».

Проверка реальностью: 99999999 отвергается с причиной, 31089986150 допускается,
наблюдение возвращает обе пары с expired=False.

Локально: 20 тестов координатора, OK.

* Proof: make the safe run the default, and test the rules that were only claimed

Two independent reviews of the final state — one of them running actual
mutants — showed this slice narrowed the incident class without closing it,
and that its own commit message overstated what was pinned.

THE POLARITY.  The incident was a forgotten `--dry-run`, and the fix left the
dangerous run as the default: safety still meant remembering a flag.  Printing
is now what happens, `--execute` is the opt-in, and it has to name the
campaign's size with `--expect-lanes`.  Swapping the two widths is a realistic
slip — `--lane-width 256 --shard-width 4` is 65536 runs — and the only cheap
moment to notice is before the first invocation.

THE ARTIFACT COORDINATE.  A mutant hardcoding `EVIDENCE_ARTIFACTS_V1[0]` at
the admission call site survived all twenty tests: both seam tests always
passed the arb name, so pinning one engine was indistinguishable.  The
producer runs its two engines as independent jobs, so a run really can carry
one artifact and not the other, and admitting the wrong one buys 256 doomed
lanes — the incident down a second axis.  The previous message claimed this
coordinate was pinned; it was pinned where commands are built, not where the
admission asks.

THE EXPIRY RULE.  Moving the filter out of the query put the decision where a
test could reach it and left the decoding that produces it behind the network.
Two mutants survived: one that never marks anything expired, one that drops
the field from the query.  `parse_artifact_listing_v1` is pure now and refuses
a listing that is not the two-column form it asked for — the old parser read
an unrecognised line as "live", which admits exactly the stale evidence run
this gate exists to refuse.  The repository currently holds 127 expired
artifacts, so the path is not hypothetical.

A campaign that dies mid-flight now reports how many lanes it launched and
returns the typed exit instead of a traceback, so the retry can resume rather
than duplicate.

Every repair is proven by the mutant it kills, each failing exactly one test:
the hardcoded artifact, the never-expired listing, the field dropped from the
query, and `repr(error)` in place of the stderr that tells "no such run" from
"no token".

Verified: 276 local tests, 7 errors — the exact Windows baseline.

Left out deliberately, and not hidden: the admission still checks that the
named artifact exists, not that it carries `job.bin` and the comparator
bundle, and not which workflow or commit produced it.  Those are a different
class — supply-chain provenance rather than operator error — and want their
own slice.

* Proof: pin the two new handles, and the coordinate whose test claimed it was pinned

A third review ran fifteen mutants against the previous commit; nine survived,
and all but one lived in the code that commit added.  The polarity change and
the partial-failure report were shipped without a single test that could hold
them.

THE RUN ID.  All three seam tests take the run id from one helper that
hardcodes 424242, so a call site hardcoding that same number was
indistinguishable from one that reads the argument — while the test's own
comment claimed the opposite in as many words.  The mirror axis was fixed last
round and this one left standing.  A seam test now names a different run and
records what the observer was actually asked.

THE DECLARED SCALE.  Making `--expect-lanes` optional survived every test,
because falling through to the mismatch comparison refuses too — with a
message that tells the operator nothing.  It has its own refusal now, and the
test reads the message rather than only the exit code: the two differ exactly
where the operator does.

THE PARTIAL FAILURE.  Deleting the whole handler, returning 0 mid-campaign, or
moving the counter before the call all stayed green.  The count is the point —
a retry that cannot tell three from zero duplicates everything already
dispatched — so the test now reads "after 3 of 256" out of the report.  The
handler also catches OSError: a missing `gh` abandoned a campaign with a
traceback and no count.

ORDER.  The authenticated observation ran before the free local check, so a
mistyped width spent an API call and answered with a diagnosis about the
evidence run.  The cheap refusals come first now, which is what the comment
claimed all along.

`--mode plan --execute` was a silent no-op: a flag with live semantics that is
quietly ignored is worse than one that refuses, because the operator believes
a campaign started.

`--paginate` was checked by reading the function's source, which a mutant that
drops the flag from argv and leaves the word in a comment passes untouched.
The check is behavioural now — it asserts the argv actually built, plus
`check` and `capture_output`, which the source read never saw.

Names that referred to the removed flag are gone; only the comment recounting
the incident still says `--dry-run`, which is history and stays.

Every repair is proven by the mutant it kills, each failing exactly one test:
the hardcoded run id, the optional scale, the vanished counter, `return 0`
mid-campaign, the ignored `--execute` in plan mode, and `--paginate` demoted
to a comment.

Verified: 282 local tests, 7 errors — the exact Windows baseline.

* Proof: a dispatch admits the run's origin, not only the artifact's name

The evidence admission checked that a live artifact of the named name
existed inside the named run.  Three runs pass that and must not: one
produced by a different workflow, one a fork's pull request produced —
carrying a comparator bundle nobody reviewed — and one that never finished
successfully.  Each buys 256 lanes replaying something the operator did not
intend, and the reproduction is exit 0 with 256 dispatches while nothing was
ever asked about the run.

The run is now observed as a projection of five named fields (never the run
object: an unknown reply is how this project has leaked), decoded by a rule
that refuses shape drift instead of defaulting, and admitted by a pure rule
covering the workflow path, the workflow_dispatch trigger, completion with
conclusion success, and a readable head_sha.  Any failure to observe is a
typed refusal, as with the artifact.

Staleness cannot be decided here — last week's green producer run satisfies
every rule above — so the one coordinate that settles it is put in front of
the operator at the moment of admission, before the lanes start.

Twelve mutants of the new rules were run against the suite and all twelve
were killed, including a dropped seam call in `main`, a fail-open
observation, a decode that invents missing fields, and a query that fetches
the whole reply.

The artifact's *contents* are still unchecked: proving job.bin and the
comparator bundle exist requires downloading it, and that is its own slice.

* Proof: pin what the origin admission only claimed

Independent verification ran mutants against the previous commit and found
three rules that lived in the code and in comments beside it, with nothing to
hold them.

`text=True` was the dangerous one.  Removing it makes `gh` return bytes, the
decode raises, and every run is refused — a gate that looks alive and admits
nobody, fail-closed and silent.  No test saw it, and the same gap existed for
the artifact observation, so both are pinned now.

The comment claimed origin is asked before contents because a run of the wrong
workflow lists an artifact of exactly the right name.  True, and unproven:
swapping the two blocks left everything green.  The order is the claim, so the
order is what the test reads — the artifact observer must never be asked when
the origin is refused.

`RunProvenanceV1.__post_init__` was unreachable — the parser only ever feeds it
string slices — and raised a bare `TypeError` in a module whose whole thesis is
typed refusals.  Deleting it left the suite green, which is the definition of
speculative defence.  Gone.

Proven by the mutant each kills, one test apiece: the missing `text=True`, and
the swapped gate order.

Verified: 54 dispatch tests pass; 452 on Linux with the three pre-existing
`test_build` failures that also fail on an untouched tree.

This commit was authored on the #548 branch by mistake and moved to its own
before anything was pushed — #548 has been through three review rounds and
does not need a new feature landing inside it.

---------

Co-authored-by: Claude Code <daniilerosov12@gmail.com>
lemone112 added a commit that referenced this pull request Aug 7, 2026
…e (V5b2d-4g) (#555)

* Proof: a dispatch admits the evidence its lanes can read, not the name (V5b2d-4g)

The verification dispatch binds an evidence run and an exact artifact name,
and a name is not a layout.  An evidence run built by an older producer,
with a different evidence-out/ shape, carries the right artifact name and
the wrong contents: it passes admission, fans out to 256 lanes, and every
one of them dies seconds in on `test -f`.  That is the incident of the 133
wasted runs again, on its third axis.

So the admission reads what it names.  The coordinator downloads the bound
artifact once — two files well under a megabyte — and dispatches only when
it carries every path the lane workflow guards.

The module's shape is kept: the download is the one impure boundary and
decides nothing; which paths must be there is a pure rule a test can reach
without a network; a missing input and any failure to observe are the same
typed refusal, never a crash and never a silent proceed.  The temporary
directory is owned by the download and removed on both paths.  A dry run
stays offline by contract.

The path list is a contract with verification-lanes.yml, so it is not
restated by hand: tests read it back out of the workflow text in both
directions — every guarded path must be admitted, and every evidence path
the lane uses must be covered — so the two cannot drift apart silently.

* Proof: keep the promise this slice wrote down

Final-state review anchored on the commit object — after catching its own
first mutation pass reading a stale file through WSL — ran 26 mutants and
found two blockers plus one theatre test.

THE CRASH.  A refused command set is not iterable, and `main` fell straight
into the loop with it: a mistyped artifact name — the likeliest operator slip
— crashed with a TypeError while this very commit's docstring promises never
a crash.  The same fallthrough was fixed on the #548 branch, but this branch
grows from main, where it still lived.  Typed exit now, and the CLI test
covers three foreign coordinates while also proving the expensive download
never ran for any of them — a coordinate the pure builder already refused
must not cost a network call.

THE FALSE CLAIM.  The comment said `CalledProcessError.__repr__` drops
stderr.  Measured: it does not — `BaseException` keeps every constructor
argument in `args`.  The preference for `stderr` over `repr` is legibility,
not recovery, and the comment now says so instead of inventing a stronger
reason.

THE THEATRE.  The foreign-entries test fed bytes, None and an int to the type
guard — inputs that can never equal a string, so the guard was semantically
free and the test held without the code it pointed at.  An unhashable list is
what makes the guard load-bearing: without it the observation raises instead
of refusing.  The test feeds one now, and stripping the guard fails it.

Also: the download carries a deadline.  A hung `gh` stalled the coordinator
forever on the last check standing between an operator and 256 dispatches,
indistinguishable from work in progress.

Each repair proven by its mutant: the restored fallthrough reddens the new
CLI test, the stripped guard reddens the strengthened one.

Verified: 31 dispatch tests pass; 426 on Linux with the three pre-existing
`test_build` failures that also fail on an untouched tree.

---------

Co-authored-by: Claude Code <daniilerosov12@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant