diff --git a/.cspell.json b/.cspell.json index d35bd81..1326a6b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -30,8 +30,8 @@ "claimом", "clippy", "CLIS", - "Crcs", "crcs", + "Crcs", "dalek", "deadbe", "deadbeefdeadbeefdead", @@ -55,6 +55,7 @@ "Kratos", "libc", "libmt", + "livelock", "mktemp", "mktree", "msvc", @@ -241,37 +242,11 @@ "збілджений", "збілдити", "змержити", - "квіз", - "квізом", - "квізу", - "квізів", - "колапсить", - "лаповане", - "лапованому", - "лаповані", - "лінкованого", - "лінковані", - "Мілстоун", - "мілстоун", - "мілстоунами", - "мілстоуни", - "мілстоунів", - "недовірений", - "нідедлайн", - "парсять", - "перегенеровую", - "Позамежі", - "почастинно", - "репозиторний", - "розсинхронізований", - "сабсет", - "фейлиться", - "форки", - "фронтматер", "Ідемпотентно", "ін'єктується", "ін'єктуються", "інвалідація", + "інвалідаціями", "інвалідована", "інвалідований", "інвалідованого", @@ -279,7 +254,6 @@ "інвалідовуються", "інвалідування", "Інвалідувати", - "інвалідаціями", "інвалідує", "інвентаря", "інжект", @@ -304,11 +278,16 @@ "ітеративно", "Ітеративно", "каскадує", + "квіз", + "квізів", + "квізом", + "квізу", "кворумних", "Кворумних", "кворумні", "Кворумні", "кворумної", + "колапсить", "коміт", "коміта", "коміти", @@ -336,6 +315,11 @@ "курування", "лайфсайклу", "ландингу", + "лаповане", + "лаповані", + "лапованому", + "лінковані", + "лінкованого", "лінкує", "лінкується", "лістенер", @@ -363,6 +347,11 @@ "метасимволів", "мідфлайт", "Мікроменеджер", + "мілстоун", + "Мілстоун", + "мілстоунами", + "мілстоуни", + "мілстоунів", "Мітигація", "мокати", "монорепа", @@ -380,6 +369,7 @@ "невалідних", "невалідному", "недетермінізм", + "недовірений", "неефемерні", "незакоміче", "незакомічених", @@ -396,6 +386,7 @@ "неучасників", "низьковажільне", "низьковажільні", + "нідедлайн", "ноди", "облікується", "однопакетний", @@ -409,12 +400,14 @@ "пайпі", "паралелить", "Парсимо", + "парсять", "Пасивн", "патчить", "патчів", "перiод", "перебілд", "перебілджує", + "перегенеровую", "перегенеруй", "перегенерують", "перемаршрутовуються", @@ -447,8 +440,10 @@ "платформозалежному", "побрейнштормимо", "поведі", + "Позамежі", "портабельна", "пофайловий", + "почастинно", "превʼю", "прогонить", "програмкова", @@ -486,8 +481,8 @@ "резолвнувся", "резолвнуту", "резолву", - "Реімплементація", "реімплементації", + "Реімплементація", "реімплементовано", "реімплементує", "реімплементується", @@ -502,6 +497,7 @@ "реплеїться", "реплей", "реплею", + "репозиторний", "ресканить", "ретраєм", "ретраїв", @@ -514,14 +510,16 @@ "розпарсених", "розсинхрон", "розсинхроні", + "розсинхронізований", "роутінг", "роутінговими", "роутінгових", "роутінгові", + "сабпроцесу", + "сабсет", "самопокращення", "санітизоване", "Санітизує", - "сабпроцесу", "серіалізатором", "сесійність", "синхр", @@ -570,6 +568,7 @@ "уточнюючі", "фактум", "фейл", + "фейлиться", "фейляться", "фенсить", "фенситься", @@ -580,7 +579,9 @@ "фолбек", "фолбеком", "форвардить", + "форки", "фронтенд", + "фронтматер", "фронтматері", "фронтматером", "фронтматеру", diff --git a/crates/mt-core/src/lib.rs b/crates/mt-core/src/lib.rs index 69c275a..4e5cd53 100644 --- a/crates/mt-core/src/lib.rs +++ b/crates/mt-core/src/lib.rs @@ -257,9 +257,20 @@ fn run_is_failure(path: &Path) -> bool { } } -// count(run із result ∈ execution-failure і NNN > останнього fact) — graph.md. +// «Прийнятий fact» (graph.md): актуальний fact_N (max NNN) без pending-audit_N +// або з audit-result_N: success. Відхилений аудитом fact прийнятим НЕ є, тому +// межу не рухає — інакше цикл «провал → провал → сирий fact → аудит відхилив» +// обнуляв би лічильник вічно, і драбина ніколи не дійшла б до engineer/unresolvable. +fn accepted_fact_nnn(dir: &Path) -> u64 { + match accepted_fact_state(dir) { + FactState::Resolved => max_nnn(dir, "fact_", ".md"), + FactState::PendingAudit | FactState::None => 0, + } +} + +// count(run із result ∈ execution-failure і NNN > останнього прийнятого fact) — graph.md. fn failed_streak(dir: &Path) -> u64 { - let since = max_nnn(dir, "fact_", ".md"); + let since = accepted_fact_nnn(dir); fs::read_dir(dir) .ok() .into_iter() @@ -1283,8 +1294,24 @@ mod tests { assert_eq!(state_of("task", &files, &[], None), TaskState::Waiting); } #[test] - fn streak_counts_only_runs_after_last_fact() { - // run_001 failed → fact_002 прийнято → run_003 failed: streak 1, не 2. + fn streak_counts_only_runs_after_accepted_fact() { + // fact_002 прийнято (аудит success) → run_001 не рахується, streak 1 < 2. + let files = [ + ("task.md", ""), + ("a.md", ""), + ("run_001.md", "---\nresult: failed\n---\n"), + ("fact_002.md", ""), + ("pending-audit_002.md", ""), + ("audit-result_002.md", "---\nresult: success\n---\n"), + ("run_003.md", "---\nresult: failed\n---\n"), + ]; + // Прийнятий fact → resolved має пріоритет над станом лічильника. + assert_eq!(state_of("task", &files, &[], Some(2)), TaskState::Resolved); + } + #[test] + fn rejected_fact_does_not_reset_streak() { + // fact_002 відхилено аудитом → межа 0 → рахуються run_001 і run_003: + // streak 2 ≥ agent_retry_max 2 → failed, драбина термінує. let files = [ ("task.md", ""), ("a.md", ""), @@ -1294,7 +1321,26 @@ mod tests { ("audit-result_002.md", "---\nresult: failed\n---\n"), ("run_003.md", "---\nresult: failed\n---\n"), ]; - assert_eq!(state_of("task", &files, &[], Some(2)), TaskState::Waiting); + assert_eq!(state_of("task", &files, &[], Some(2)), TaskState::Failed); + } + #[test] + fn rejected_fact_livelock_terminates() { + // Патологія, яку закриває межа «прийнятий fact»: кожен цикл дає сирий + // fact, аудит його відхиляє. Раніше лічильник обнулявся вічно. + let files = [ + ("task.md", ""), + ("a.md", ""), + ("run_001.md", "---\nresult: failed\n---\n"), + ("fact_002.md", ""), + ("pending-audit_002.md", ""), + ("audit-result_002.md", "---\nresult: failed\n---\n"), + ("run_003.md", "---\nresult: failed\n---\n"), + ("fact_004.md", ""), + ("pending-audit_004.md", ""), + ("audit-result_004.md", "---\nresult: failed\n---\n"), + ("run_005.md", "---\nresult: failed\n---\n"), + ]; + assert_eq!(state_of("task", &files, &[], None), TaskState::Failed); } // ── unresolvable ── diff --git a/docs/conformance.md b/docs/conformance.md index 118814e..0c109e4 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -30,7 +30,7 @@ | --- | --- | --- | --- | | Сканування графа, `deps/`, denylist | РЕАЛІЗОВАНО | `lib.rs` `scan_tasks`/`scan_dir` | — | | Derived-стани вузла | ЧАСТКОВО | `lib.rs` `detect_state` | `stalled` не виводиться (немає інтеграції з remote claim refs); `blocked-invalid-dep` як warning | -| `failed_streak` за категорією `result` | РЕАЛІЗОВАНО | `lib.rs` `failed_streak`/`is_execution_failure` | — (межа рахунку — останній `fact_NNN`; спека каже «останній *прийнятий* fact» — відкрите питання нижче) | +| `failed_streak`: категорія `result` + межа «прийнятий fact» | РЕАЛІЗОВАНО | `lib.rs` `failed_streak`/`is_execution_failure`/`accepted_fact_nnn` | — | | Файловий контракт `a.md`/`h.md` | ЧАСТКОВО | `lib.rs` `write_executor_flag` | Пишуться markdown-секціями, спека вимагає YAML-фронтматер; немає `secrets`, `interactive`, `assignee`, `notify`, `parent` — **конфлікт спека↔код, потребує рішення** | | Артефакти version chain (читання) | РЕАЛІЗОВАНО | `artifacts.rs` | — | | `schema_version` fail-closed | ВІДСУТНЄ | — | Поле пишеться, але жоден читач не валідує версію | @@ -114,8 +114,11 @@ 5. **M6 фаза 0 — модельний трек Дельти.** Паралельно від хвилі 2, як велить roadmap: `mandates.yaml` (включно з `kind: model` і `audacity`), `decision-request` із `leverage_facets`, `chosen_option`, стан `awaiting-decision`, квіз-гейт, конверсія вичерпаної драбини в розвилку. Соціальних ризиків нема — механіка обкатується на моделях. 6. **M3 / M5 / M4.** Dashboard і поверхні; retro (MVP не чекає M1–M4 — дані вже є); файловий шар i18n. +## Закриті питання + +- **Межа `failed_streak`** (2026-08-09): рішення — спека виграє, код приведено до неї. Межа — останній *прийнятий* fact (`accepted_fact_nnn`); відхилений аудитом fact межу не рухає. Причина: інакше цикл «провал → провал → сирий fact → аудит відхилив» обнуляє лічильник вічно, і драбина ретраїв ніколи не доходить до EngineerAgent чи `unresolvable` — livelock. Тест-сторож: `rejected_fact_livelock_terminates`. + ## Відкриті питання -- **Межа `failed_streak`.** Спека каже «NNN > останнього *прийнятого* fact»; реалізація рахує від останнього `fact_NNN` незалежно від вердикту аудиту. Розходження проявляється лише в rework-циклі після провального аудиту. Потрібне рішення: уточнити спеку чи змінити код. - **Формат `a.md`/`h.md`.** Спека вимагає YAML-фронтматер, код пише markdown-секції. Один із двох має поступитись — це контракт, який читають і люди, і агенти. - **`mt-napi`.** За рішенням Г специфікації `2026-07-23-mt-cli-rust.md` крейт мав бути видалений, але лишається у workspace members.