diff --git a/.changes/260809-1731.md b/.changes/260809-1731.md new file mode 100644 index 0000000..22b28e2 --- /dev/null +++ b/.changes/260809-1731.md @@ -0,0 +1,5 @@ +--- +bump: patch +section: Fixed +--- +docs(mandates): контракт M6 — кореневий мандат рівно один, подвійний підпис зміни escalates_to diff --git a/.cspell.json b/.cspell.json index b6030ca..5243ce7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -187,6 +187,7 @@ "дедуплікується", "декомпозовано", "декомпозує", + "делегатор", "делегатора", "делегатором", "деплой", diff --git a/docs/architecture/mandates.md b/docs/architecture/mandates.md index cdfb7ec..e45d4ef 100644 --- a/docs/architecture/mandates.md +++ b/docs/architecture/mandates.md @@ -239,9 +239,9 @@ ephemeral_mandate: | `mandates[].thresholds.risk` | enum `low` \| `medium` \| `high` | ні | відсутнє = без обмеження за рівнем ризику | той самий режим, що `budget_eur` | | `mandates[].thresholds.irreversible` | bool | ні | `false` — за замовчуванням мандат **не** покриває незворотні рішення (консервативний дефолт, узгоджений з інваріантом reversible нижче) | розширення до `true` — підписаний акт делегування; звуження до `false` — сам власник | | `mandates[].thresholds.audacity` | enum `low` \| `medium` \| `high`; **лише для `kind: model`** — для `kind: person` поле неприпустиме (validate-помилка, якщо присутнє) | ні (для `kind: model`) | `low` | розширення — **лише людина**, завжди («остання константа», [vision.md](../vision.md) «Дельта») | -| `mandates[].escalates_to` | handle власника-адресата ескалації, або `null` лише для рівно одного кореневого мандата | так | — | зміна маршруту ескалації — підписаний акт **нового** `escalates_to`-власника (він приймає на себе роль ескалаційної точки; це своя згода, не чужий підпис) | +| `mandates[].escalates_to` | handle власника-адресата ескалації, або `null` лише для рівно одного кореневого мандата | так | — | зміна маршруту ескалації — **подвійний підпис**: (а) новий `escalates_to`-адресат приймає роль (згода прийняти на себе точку ескалації); (б) делегатор рівня вище — поточний (замінюваний) `escalates_to`-власник — підтверджує саму зміну маршруту, бо інакше власник міг би односторонньо перевести нагляд на «зручнішого» наглядача, залишивши того, хто організаційно відповідає за нагляд, без відома (директорська модель, [vision.md](../vision.md) «Дельта») | -Валідатор (частина `validate_mandate_change` нижче) додатково відхиляє: більше нуля мандатів з `escalates_to: null` або дерево, де від будь-якого `owner` неможливо дійти до кореня скінченним ланцюгом `escalates_to` (цикл або висячий handle). +Валідатор (частина `validate_mandate_change` нижче) додатково відхиляє: кількість мандатів з `escalates_to: null` ≠ 1 (нуль — немає кореня, більше одного — неоднозначний корінь), а також дерево, де від будь-якого `owner` неможливо дійти до кореня скінченним ланцюгом `escalates_to` (цикл або висячий handle). ### Формат квіз-файлів @@ -303,7 +303,7 @@ Crate живе в `mt-rust` (той самий патерн, що `crates/mt-nap - **`parse_mandates(path) → MandatesFile`** — парсить `.mt/mandates.yaml`, повертає `{ generation, mandates[] }` або помилку валідації (порожній scope, більше/менше одного кореневого мандата, цикл ескалації, `audacity` на `kind: person`). - **`effective_owner(node_ref, decision_type, facets) → { owner, kind, escalation_chain[] }`** — lookup за `refs`×`decision_types`×`thresholds` (крок 1 маршрутизатора вище); повертає обчисленого власника і повний ланцюг `escalates_to` до кореня для автопідйому по SLA. -- **`validate_mandate_change(diff, signature) → Verdict`** — `Verdict = Valid | Invalid(reason)`. Перевіряє: `generation` файлу зріс рівно на 1; будь-яке розширення `scope`/`thresholds` підписане делегатором рівня вище (окрім звуження — самопідпис); розширення `kind: model` мандата (включно з підняттям `audacity`) підписане **лише** людським ключем — модельний підпис на такому дифі відхиляється безумовно («остання константа»). +- **`validate_mandate_change(diff, signature) → Verdict`** — `Verdict = Valid | Invalid(reason)`. Перевіряє: `generation` файлу зріс рівно на 1; будь-яке розширення `scope`/`thresholds` підписане делегатором рівня вище (окрім звуження — самопідпис); зміна `escalates_to` вимагає **обох** підписів — нового адресата й поточного (замінюваного) делегатора; розширення `kind: model` мандата (включно з підняттям `audacity`) підписане **лише** людським ключем — модельний підпис на такому дифі відхиляється безумовно («остання константа»); кількість кореневих мандатів (`escalates_to: null`) — рівно 1. - **`validate_approval(response, quiz) → Verdict`** — перевіряє наявність і завершеність `quiz_ref` для `decision-request`-гейту, відповідність `quiz.decision_ref` вузлу з `response`, `quiz.depth` — мапінгу з `leverage_facets`, і сам Ed25519-підпис проти pubkey-кешу (той самий шлях, що інші два гейти — [access.md](access.md)). ## Місце в roadmap