feat: E-S1 gate-3 safe core buildGate3Product and decideGate3Closure
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# E-S1 / gate-3 безопасное ядро — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development.
|
||||
|
||||
**Goal:** Чистое ядро третьего гейта «цель достигнута?»: `buildGate3Product` (продукт для судьи) +
|
||||
`decideGate3Closure` (замыкание петли через `loopTerminationDecision`), без триггера-в-стене.
|
||||
|
||||
**Architecture:** Обе функции — в существующий `tools/loop-termination.mjs` (его естественный дом:
|
||||
он и есть модуль закрытия петли; `decideGate3Closure` зовёт `loopTerminationDecision` из того же
|
||||
файла). Чистые, детерминированные, модель/стену НЕ трогают. Спека v2 §g5 называла отдельные
|
||||
модули — план осознанно консолидирует в loop-termination.mjs по решению владельца (избежать
|
||||
override на новый production-файл); ВСЕ контракты §t1/§g1/§g2 honored побайтно, имена функций и
|
||||
поведение те же.
|
||||
|
||||
**Tech Stack:** Node ESM, vitest. Источник истины — spec `2026-06-17-es1-gate3-safe-core-design-v2.md`.
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Реализовать §g2 (`buildGate3Product`) + §g1 (`decideGate3Closure`) из spec v2, с аутентификацией
|
||||
входов §t1 (degraded никогда не закрывает; контроллер без подписи закрыть не может). Триггер-в-стене
|
||||
и «зубы» — вне объёма (§g6, спека #2).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Тесты ядра gate-3 (RED)
|
||||
|
||||
**Files:** Test: `tools/loop-termination.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Дописать describe-блоки** (Edit — добавить в конец файла):
|
||||
|
||||
```js
|
||||
import { buildGate3Product, decideGate3Closure } from './loop-termination.mjs';
|
||||
|
||||
describe('gate-3 buildGate3Product (E-S1 §g2)', () => {
|
||||
it('цель+шаги+greens → продукт со сводкой исполнения и green-метками', () => {
|
||||
const r = buildGate3Product({
|
||||
goal: 'добавить хелпер',
|
||||
planSteps: [{ id: 's1', op: 'Write', object: 'tools/x.mjs' }],
|
||||
greenRuns: [{ stepId: 's1', criterion: 'x-test' }],
|
||||
});
|
||||
expect(r.goal).toBe('добавить хелпер');
|
||||
expect(r.product).toMatch(/Write tools\/x\.mjs/);
|
||||
expect(r.product).toMatch(/green: x-test/);
|
||||
expect(r.cards).toEqual([]);
|
||||
});
|
||||
it('нет greens → пометка «нет доказательств исполнения»', () => {
|
||||
const r = buildGate3Product({ goal: 'g', planSteps: [{ op: 'Bash', object: 'echo a' }], greenRuns: [] });
|
||||
expect(r.product).toMatch(/нет доказательств исполнения/);
|
||||
});
|
||||
it('пустой ввод → не бросает, форма {product,goal,cards}', () => {
|
||||
const r = buildGate3Product({});
|
||||
expect(r).toEqual({ product: expect.stringMatching(/нет доказательств/), goal: '', cards: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('gate-3 decideGate3Closure (E-S1 §g1/§g4/§t1)', () => {
|
||||
it('degraded судья (wired:false) без accept → negotiate, НЕ closed', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: false, decision: 'GO' } });
|
||||
expect(r.state).toBe('negotiate');
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
it('подписанный владелец accept → closed+terminate (приоритет над судьёй)', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: false }, ownerArbitration: 'accept' });
|
||||
expect(r.state).toBe('closed');
|
||||
expect(r.terminate).toBe(true);
|
||||
});
|
||||
it('реальный GO движка → closed+terminate', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' } });
|
||||
expect(r.state).toBe('closed');
|
||||
expect(r.terminate).toBe(true);
|
||||
});
|
||||
it('владелец continue → open', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, ownerArbitration: 'continue' });
|
||||
expect(r.state).toBe('open');
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
it('NO-GO & круг<3 → negotiate', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 1 });
|
||||
expect(r.state).toBe('negotiate');
|
||||
});
|
||||
it('NO-GO & круг>=3 → arbitrate + карточка', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 3 });
|
||||
expect(r.state).toBe('arbitrate');
|
||||
expect(r.card).toBe(true);
|
||||
});
|
||||
it('контроллер без подписи (нет accept, NO-GO) закрыть не может', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 0 });
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Прогнать тесты gate-3 — ожидать RED**
|
||||
|
||||
Run: `npx vitest run tools/loop-termination.test.mjs -t "gate-3" --config vitest.config.tools.mjs`
|
||||
Expected: FAIL — `buildGate3Product`/`decideGate3Closure` не определены.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Реализация ядра (GREEN)
|
||||
|
||||
**Files:** Modify: `tools/loop-termination.mjs`
|
||||
|
||||
- [ ] **Step 3: Добавить обе функции** (Edit — добавить в конец файла, после `loopTerminationDecision`):
|
||||
|
||||
```js
|
||||
|
||||
/**
|
||||
* E-S1 §g2: продукт судьи gate-3 (цель достигнута?) — детерминированная сводка исполненного:
|
||||
* цель + шаги + их по-критерию GREEN. Форма совместима с buildJudgePrompt (judge-engine).
|
||||
*/
|
||||
export function buildGate3Product({ goal = '', planSteps = [], greenRuns = [] } = {}) {
|
||||
const greenById = new Map();
|
||||
for (const g of Array.isArray(greenRuns) ? greenRuns : []) {
|
||||
if (g && g.stepId != null) greenById.set(String(g.stepId), g.criterion ?? true);
|
||||
}
|
||||
const lines = (Array.isArray(planSteps) ? planSteps : []).map((s, i) => {
|
||||
const id = s && s.id != null ? String(s.id) : String(i);
|
||||
const green = greenById.has(id) ? greenById.get(id) : null;
|
||||
return { text: `${s && s.op} ${s && s.object}`, green };
|
||||
});
|
||||
const hasEvidence = lines.some((l) => l.green != null);
|
||||
const product = hasEvidence
|
||||
? lines.map((l) => `${l.text}${l.green != null ? ` [green: ${l.green}]` : ''}`).join('\n')
|
||||
: 'нет доказательств исполнения (шаги не подтверждены по-критерию)';
|
||||
return { product, goal: String(goal || ''), cards: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 §g1/§g4: оркестратор замыкания внешней петли. Закрытие ТОЛЬКО через loopTerminationDecision
|
||||
* (SE-R7-6). Degraded судья (wired:false) НИКОГДА не закрывает/не арбитрит — fail-safe против
|
||||
* ложного исхода на сбое транспорта. Входы аутентифицирует потребитель (§t1: ownerArbitration —
|
||||
* только из подписанного канала владельца; gate3Verdict — только выход движка); функция чистая.
|
||||
*/
|
||||
export function decideGate3Closure({ gate3Verdict = null, noGoCount = 0, ownerArbitration = null, maxRounds = 3 } = {}) {
|
||||
const degraded = !!gate3Verdict && gate3Verdict.wired === false;
|
||||
if (degraded && ownerArbitration !== 'accept') {
|
||||
return { state: 'negotiate', terminate: false, reason: 'судья gate-3 недоступен — не закрывать, повтор/доработка' };
|
||||
}
|
||||
if (ownerArbitration === 'accept') {
|
||||
const t = loopTerminationDecision({ ownerDeclaredDone: true });
|
||||
return { state: 'closed', terminate: t.terminate, reason: t.reason };
|
||||
}
|
||||
if (gate3Verdict && gate3Verdict.decision === 'GO' && gate3Verdict.wired !== false) {
|
||||
const t = loopTerminationDecision({ judgeGate3Go: true });
|
||||
return { state: 'closed', terminate: t.terminate, reason: t.reason };
|
||||
}
|
||||
if (ownerArbitration === 'continue') {
|
||||
return { state: 'open', terminate: false, reason: 'владелец выбрал продолжать' };
|
||||
}
|
||||
const rc = (Number.isInteger(noGoCount) && noGoCount >= 0) ? noGoCount : maxRounds;
|
||||
if (rc < maxRounds) {
|
||||
return { state: 'negotiate', terminate: false, reason: `gate-3 NO-GO; круг ${rc}/${maxRounds} — переговоры` };
|
||||
}
|
||||
return { state: 'arbitrate', terminate: false, reason: `gate-3 NO-GO ${maxRounds} круга — арбитраж владельцу`, card: true };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Прогнать весь файл — ожидать GREEN**
|
||||
|
||||
Run: `npx vitest run tools/loop-termination.test.mjs --config vitest.config.tools.mjs`
|
||||
Expected: PASS (gate-3 тесты + существующие loopTerminationDecision).
|
||||
|
||||
- [ ] **Step 5: Полная регрессия**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs --no-file-parallelism`
|
||||
Expected: PASS (база зелёная + новые тесты; существующие модули не трогались → 0 регрессий).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Покрытие spec:** §t1 (аутентификация входов) → decideGate3Closure degraded+accept-приоритет +
|
||||
тест «контроллер без подписи не закрывает»; §g1 (контракт/порядок проверок) → Task 2 Step 3 +
|
||||
тесты closure; §g2 (продукт) → buildGate3Product + тесты; §g4 (terminate только из хелпера) →
|
||||
closed-ветки зовут loopTerminationDecision; §g7 (критерий) → Steps 2/4/5. §g6 (триггер/зубы) —
|
||||
вне объёма, не реализуется (верно).
|
||||
- **Плейсхолдеров нет** — весь код приведён.
|
||||
- **Согласованность имён:** `buildGate3Product({goal,planSteps,greenRuns})→{product,goal,cards}`;
|
||||
`decideGate3Closure({gate3Verdict,noGoCount,ownerArbitration,maxRounds})→{state,terminate,reason,card?}`;
|
||||
`loopTerminationDecision` — из того же файла (hoisted, импорт не нужен). Едины во всех тасках.
|
||||
- **Отступление от спеки (осознанное):** код в `loop-termination.mjs`, не в отдельных gate3-*.mjs
|
||||
(§g5) — решение владельца ради избежания override на новый production-файл; контракты не изменены.
|
||||
|
||||
```skills-json
|
||||
["test-driven-development"]
|
||||
```
|
||||
|
||||
```steps-json
|
||||
[
|
||||
{"op":"Edit","object":"tools/loop-termination.test.mjs","ref":"g7"},
|
||||
{"op":"Bash","object":"npx vitest run tools/loop-termination.test.mjs -t \"gate-3\" --config vitest.config.tools.mjs","ref":"g7"},
|
||||
{"op":"Edit","object":"tools/loop-termination.mjs","ref":"g1"},
|
||||
{"op":"Bash","object":"npx vitest run tools/loop-termination.test.mjs --config vitest.config.tools.mjs","ref":"g2"},
|
||||
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs --no-file-parallelism","ref":"g7"}
|
||||
]
|
||||
```
|
||||
|
||||
```verified-context-json
|
||||
[{"id":"pc1","kind":"EXTRACTED","ref":"tools/loop-termination.mjs","anchor":"loopTerminationDecision"}]
|
||||
```
|
||||
@@ -0,0 +1,136 @@
|
||||
# E-S1 / gate-3 безопасное ядро — судья «цель достигнута» + замыкание петли (v2)
|
||||
|
||||
**Дата:** 2026-06-17
|
||||
**Эпик:** роутер-наставник, E-S1 (sub-plan C, ядро). **Статус:** дизайн под реализацию (TDD).
|
||||
**Кодовая фраза:** «роутер-наставник».
|
||||
|
||||
## Цель
|
||||
|
||||
Сейчас машинерия судит спеку (gate1) и план (gate2) ДО работы, но никогда не проверяет ПОСЛЕ —
|
||||
достиг ли результат заявленной цели. Внешняя петля закрывается фактически по усмотрению контроллера.
|
||||
Хелпер `loopTerminationDecision` уже задаёт правило «петлю закрывает ТОЛЬКО владелец явно ЛИБО GO
|
||||
судьи gate-3», но он нигде не потреблён. Эта спека строит **безопасное ядро** третьего гейта:
|
||||
чистый движок-судью «цель достигнута?», его переговоры+арбитраж (симметрично gate1/gate2) и
|
||||
оркестратор замыкания петли через `loopTerminationDecision`. Автозапуск gate-3 на завершении плана
|
||||
(триггер внутри стены) — НЕ здесь (см. «Вне объёма»). Ядро — чистая логика, тестируемая в изоляции;
|
||||
кода стены НЕ трогает.
|
||||
|
||||
## Доверие входов (анти-обход) {#t1}
|
||||
|
||||
Ядро чистое (принимает параметры), но безопасность держит контракт ИСТОЧНИКА входов — иначе
|
||||
контроллер подделал бы «владелец принял» и закрыл петлю в обход (класс премортем: abuse/seam):
|
||||
|
||||
- `ownerArbitration` принимается ТОЛЬКО из подписанного канала владельца — запись
|
||||
`askuser-decisions-<sess>.jsonl`, подписанная ключом владельца (`router-mentor-receipts`),
|
||||
как floor_escape/approve. Потребитель ОБЯЗАН проверить подпись ДО вызова `decideGate3Closure`;
|
||||
непроверенное/неподписанное `ownerArbitration` → передаётся как `null` (отсутствует). Контроллер
|
||||
ключа не имеет → подделать «accept» не может (канал protected).
|
||||
- `gate3Verdict` принимается ТОЛЬКО как выход реального движка судьи (`runJudge`,
|
||||
`functionName='gate3'`), не синтезируется контроллером. Любой не-движковый/битый объект →
|
||||
трактуется как НЕ-GO (сомнение → не закрывать).
|
||||
- Инвариант SE-R7-6 сохранён: закрыть петлю может ТОЛЬКО подписанный владелец ЛИБО реальный судья
|
||||
gate-3; контроллер — никогда, ни прямо, ни подделкой входа.
|
||||
|
||||
## Контракт оркестратора замыкания {#g1}
|
||||
|
||||
Новый чистый модуль `tools/gate3-closure.mjs`, функция
|
||||
`decideGate3Closure({ gate3Verdict, noGoCount = 0, ownerArbitration = null, maxRounds = 3 })`
|
||||
→ `{ state, terminate, reason, card? }`, `state ∈ {'closed','negotiate','arbitrate','open'}`.
|
||||
Входы уже аутентифицированы потребителем ({#t1}); функция чистая и детерминированная. Порядок
|
||||
проверок (приоритет — сверху вниз):
|
||||
|
||||
1. `gate3Verdict.wired === false` (судья degraded/недоступен) И `ownerArbitration !== 'accept'`
|
||||
→ `{ state:'negotiate', terminate:false, reason:'судья недоступен — не закрывать, повтор/доработка' }`
|
||||
(degraded НИКОГДА не закрывает и не уходит в арбитраж — fail-safe против ложного исхода).
|
||||
2. `ownerArbitration === 'accept'` → `loopTerminationDecision({ ownerDeclaredDone: true })` →
|
||||
`{ state:'closed', terminate:true }` (подписанный владелец принял — приоритет над судьёй).
|
||||
3. `gate3Verdict.decision === 'GO'` (и `wired !== false`) →
|
||||
`loopTerminationDecision({ judgeGate3Go: true })` → `{ state:'closed', terminate:true }`.
|
||||
4. `ownerArbitration === 'continue'` → `{ state:'open', terminate:false }` (владелец: продолжать).
|
||||
5. NO-GO и `noGoCount < maxRounds` → `{ state:'negotiate', terminate:false }`
|
||||
(контроллер отвечает на возражение: доказывает ИЛИ доделывает; петля НЕ закрыта).
|
||||
6. NO-GO и `noGoCount >= maxRounds` → `{ state:'arbitrate', terminate:false, card }`
|
||||
(карточка арбитража владельцу — {#g3}).
|
||||
|
||||
`terminate` ВСЕГДА берётся из `loopTerminationDecision` (единый источник правила), не вычисляется
|
||||
заново; строгий `===true` хелпера сохранён. Пустой/битый `gate3Verdict` (не из движка) → шаг 5/6
|
||||
как NO-GO. Деградированный судья никогда не даёт `closed`/`arbitrate` (только negotiate) — арбитраж
|
||||
только на СОДЕРЖАТЕЛЬНОМ 3-м NO-GO, не на сбое транспорта.
|
||||
|
||||
## Продукт судьи gate-3 {#g2}
|
||||
|
||||
Новый чистый модуль `tools/gate3-product.mjs`, функция
|
||||
`buildGate3Product({ goal = '', planSteps = [], greenRuns = [] })` → `{ product, goal, cards }`
|
||||
в форме, которую принимает `buildJudgePrompt` (`judge-engine.mjs`). Судья gate-3 слеп к переписке —
|
||||
ему подаётся ТОЛЬКО продукт+цель. Состав:
|
||||
|
||||
- `goal` — цель из опечатанной спеки (та же `extractGoal`, что у gate1/gate2).
|
||||
- `product` — детерминированная сводка ИСПОЛНЕННОГО: шаги плана (op+object) и для каждого значимого
|
||||
шага его подтверждённый по-критерию GREEN из `greenRuns` критерий-гейта (тест прошёл И мутация
|
||||
убита). Это «поведение», которое судья сверяет с целью (линза `behavior_vs_goal`).
|
||||
- `cards` — пусто в ядре.
|
||||
|
||||
Линзы gate-3 уже объявлены в `judge-engine.VOTE_LAYOUTS.gate3` =
|
||||
`['goal_achieved','premortem_whole','behavior_vs_goal']` — движок не трогаем, зовём
|
||||
`runJudge`/`buildJudgePrompt` с `functionName='gate3'`. `buildGate3Product` детерминирован, тестуем
|
||||
без модели; пустой план/нет greens → product помечает «нет доказательств исполнения» (судья увидит
|
||||
и, скорее всего, NO-GO — цель не доказана).
|
||||
|
||||
## Переговоры + арбитраж gate-3 {#g3}
|
||||
|
||||
gate-3 NO-GO симметричен gate1/gate2 — переговорный блок, не обрыв:
|
||||
|
||||
- **Счётчик NO-GO gate-3** — переиспользуем существующий `mentor-nogo-counter.mjs` (независимый
|
||||
счётчик по ключу стадии `gate3`). Инкремент на каждом СОДЕРЖАТЕЛЬНОМ NO-GO (degraded не считается —
|
||||
{#g1} шаг 1).
|
||||
- **Раздел «Переговоры»** — контроллер отвечает на возражение судьи (`negotiation-section.mjs`):
|
||||
доказывает достижение цели ЛИБО доделывает (новый план, который сам проходит gate1/gate2 и по
|
||||
завершении снова приводит к gate-3).
|
||||
- **Карточка арбитража на 3-м NO-GO** — `arbitration-card.buildArbitrationCard`: дословно возражение
|
||||
судьи + довод контроллера + три выбора владельцу: «принять как достигнуто» / «продолжать» /
|
||||
«объясни подробнее». Выбор возвращается в `decideGate3Closure` как `ownerArbitration` ТОЛЬКО через
|
||||
подписанный канал ({#t1}). Новых каналов сигнала владельца ядро не вводит.
|
||||
|
||||
## Замыкание петли {#g4}
|
||||
|
||||
`decideGate3Closure` — единственная точка решения «петля закрыта», и закрытие идёт ТОЛЬКО через
|
||||
`loopTerminationDecision` (инвариант SE-R7-6). `terminate:true` (state `closed`) достижим ИСКЛЮЧИТЕЛЬНО
|
||||
по подписанному `ownerArbitration='accept'` ({#t1} шаг 2) ИЛИ реальному GO движка ({#g1} шаг 3) —
|
||||
никогда контроллером, никогда на degraded. `negotiate`/`arbitrate`/`open` — петля открыта, работа
|
||||
продолжается. **Ядро НЕ вводит блокировок в стене** (нет «зуба»): «зуб» (запрет «готово» при открытой
|
||||
петле) реализует ТРИГГЕР (отдельная спека), потребляя `state`/`terminate` ядра. Это сознательная
|
||||
граница безопасной декомпозиции: ядро = безопасное fail-safe-решение, триггер = его энфорсмент.
|
||||
|
||||
## Конвенция {#g5}
|
||||
|
||||
ES-модули `tools/gate3-product.mjs`, `tools/gate3-closure.mjs`. Чистые экспортируемые функции,
|
||||
тестируемы в изоляции (модель/I/O не трогают). Переиспользование без модификации: `judge-engine`
|
||||
(`buildJudgePrompt`/`runJudge`/`VOTE_LAYOUTS.gate3`), `arbitration-card`, `mentor-nogo-counter`,
|
||||
`negotiation-section`, `loop-termination`. Без новых зависимостей. Стена (`enforce-supreme-gate`) и
|
||||
живые хуки НЕ трогаются.
|
||||
|
||||
## Вне объёма (→ спека #2) {#g6}
|
||||
|
||||
- **Триггер gate-3 на завершении плана**: ловля `planComplete` в `enforce-supreme-gate` и автозапуск
|
||||
gate-3 — трогает живую стену (риск F-J-капкана самомодификации), отдельной спекой.
|
||||
- **«Зубы» блокировки** «готово» при открытой петле — реализует триггер, потребляя `state` ядра.
|
||||
- **Проверка подписи owner-arbitration в живом хуке** ({#t1}) — wiring в потребителе (триггер);
|
||||
ядро лишь принимает уже-проверенное значение.
|
||||
|
||||
## Критерий приёмки {#g7}
|
||||
|
||||
TDD-тесты (новые `gate3-product.test.mjs`, `gate3-closure.test.mjs`):
|
||||
|
||||
- `buildGate3Product`: цель+шаги+greens → продукт несёт сводку исполнения; пустой план/нет greens →
|
||||
«нет доказательств»; форма совместима с `buildJudgePrompt`.
|
||||
- `decideGate3Closure`: degraded (`wired:false`) → negotiate, НЕ closed/arbitrate; signed
|
||||
ownerArbitration 'accept' → closed+terminate (приоритет); GO → closed+terminate; 'continue' →
|
||||
open; NO-GO & count<3 → negotiate; NO-GO & count>=3 → arbitrate+card; битый/не-движковый вердикт →
|
||||
как NO-GO; `terminate` всегда из `loopTerminationDecision`; контроллер без подписи закрыть не может.
|
||||
- Интеграция (без модели): product→prompt(`functionName='gate3'`)→mock-вердикт→closure.
|
||||
- Полная регрессия tools GREEN: `npx vitest run --config vitest.config.tools.mjs --no-file-parallelism`
|
||||
(база зелёная + новые тесты; существующие модули не модифицируются → 0 регрессий).
|
||||
|
||||
```verified-context-json
|
||||
[{"id":"vc1","kind":"EXTRACTED","ref":"tools/loop-termination.mjs","anchor":"loopTerminationDecision"}]
|
||||
```
|
||||
@@ -10,3 +10,53 @@ export function loopTerminationDecision({ ownerDeclaredDone = false, judgeGate3G
|
||||
if (judgeGate3Go === true) return { terminate: true, reason: 'судья gate-3 goal_achieved → GO на завершение' };
|
||||
return { terminate: false, reason: 'продолжать — контроллер сам не закрывает петлю (SE-R7-6)' };
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 §g2: продукт судьи gate-3 (цель достигнута?) — детерминированная сводка исполненного:
|
||||
* цель + шаги + их по-критерию GREEN. Форма совместима с buildJudgePrompt (judge-engine).
|
||||
*/
|
||||
export function buildGate3Product({ goal = '', planSteps = [], greenRuns = [] } = {}) {
|
||||
const greenById = new Map();
|
||||
for (const g of Array.isArray(greenRuns) ? greenRuns : []) {
|
||||
if (g && g.stepId != null) greenById.set(String(g.stepId), g.criterion ?? true);
|
||||
}
|
||||
const lines = (Array.isArray(planSteps) ? planSteps : []).map((s, i) => {
|
||||
const id = s && s.id != null ? String(s.id) : String(i);
|
||||
const green = greenById.has(id) ? greenById.get(id) : null;
|
||||
return { text: `${s && s.op} ${s && s.object}`, green };
|
||||
});
|
||||
const hasEvidence = lines.some((l) => l.green != null);
|
||||
const product = hasEvidence
|
||||
? lines.map((l) => `${l.text}${l.green != null ? ` [green: ${l.green}]` : ''}`).join('\n')
|
||||
: 'нет доказательств исполнения (шаги не подтверждены по-критерию)';
|
||||
return { product, goal: String(goal || ''), cards: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 §g1/§g4: оркестратор замыкания внешней петли. Закрытие ТОЛЬКО через loopTerminationDecision
|
||||
* (SE-R7-6). Degraded судья (wired:false) НИКОГДА не закрывает/не арбитрит — fail-safe против
|
||||
* ложного исхода на сбое транспорта. Входы аутентифицирует потребитель (§t1: ownerArbitration —
|
||||
* только из подписанного канала владельца; gate3Verdict — только выход движка); функция чистая.
|
||||
*/
|
||||
export function decideGate3Closure({ gate3Verdict = null, noGoCount = 0, ownerArbitration = null, maxRounds = 3 } = {}) {
|
||||
const degraded = !!gate3Verdict && gate3Verdict.wired === false;
|
||||
if (degraded && ownerArbitration !== 'accept') {
|
||||
return { state: 'negotiate', terminate: false, reason: 'судья gate-3 недоступен — не закрывать, повтор/доработка' };
|
||||
}
|
||||
if (ownerArbitration === 'accept') {
|
||||
const t = loopTerminationDecision({ ownerDeclaredDone: true });
|
||||
return { state: 'closed', terminate: t.terminate, reason: t.reason };
|
||||
}
|
||||
if (gate3Verdict && gate3Verdict.decision === 'GO' && gate3Verdict.wired !== false) {
|
||||
const t = loopTerminationDecision({ judgeGate3Go: true });
|
||||
return { state: 'closed', terminate: t.terminate, reason: t.reason };
|
||||
}
|
||||
if (ownerArbitration === 'continue') {
|
||||
return { state: 'open', terminate: false, reason: 'владелец выбрал продолжать' };
|
||||
}
|
||||
const rc = (Number.isInteger(noGoCount) && noGoCount >= 0) ? noGoCount : maxRounds;
|
||||
if (rc < maxRounds) {
|
||||
return { state: 'negotiate', terminate: false, reason: `gate-3 NO-GO; круг ${rc}/${maxRounds} — переговоры` };
|
||||
}
|
||||
return { state: 'arbitrate', terminate: false, reason: `gate-3 NO-GO ${maxRounds} круга — арбитраж владельцу`, card: true };
|
||||
}
|
||||
|
||||
@@ -22,3 +22,63 @@ describe('loopTerminationDecision (SE-R7-6)', () => {
|
||||
expect(loopTerminationDecision({ judgeGate3Go: 1 }).terminate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
import { buildGate3Product, decideGate3Closure } from './loop-termination.mjs';
|
||||
|
||||
describe('gate-3 buildGate3Product (E-S1 §g2)', () => {
|
||||
it('цель+шаги+greens → продукт со сводкой исполнения и green-метками', () => {
|
||||
const r = buildGate3Product({
|
||||
goal: 'добавить хелпер',
|
||||
planSteps: [{ id: 's1', op: 'Write', object: 'tools/x.mjs' }],
|
||||
greenRuns: [{ stepId: 's1', criterion: 'x-test' }],
|
||||
});
|
||||
expect(r.goal).toBe('добавить хелпер');
|
||||
expect(r.product).toMatch(/Write tools\/x\.mjs/);
|
||||
expect(r.product).toMatch(/green: x-test/);
|
||||
expect(r.cards).toEqual([]);
|
||||
});
|
||||
it('нет greens → пометка «нет доказательств исполнения»', () => {
|
||||
const r = buildGate3Product({ goal: 'g', planSteps: [{ op: 'Bash', object: 'echo a' }], greenRuns: [] });
|
||||
expect(r.product).toMatch(/нет доказательств исполнения/);
|
||||
});
|
||||
it('пустой ввод → не бросает, форма {product,goal,cards}', () => {
|
||||
const r = buildGate3Product({});
|
||||
expect(r).toEqual({ product: expect.stringMatching(/нет доказательств/), goal: '', cards: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('gate-3 decideGate3Closure (E-S1 §g1/§g4/§t1)', () => {
|
||||
it('degraded судья (wired:false) без accept → negotiate, НЕ closed', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: false, decision: 'GO' } });
|
||||
expect(r.state).toBe('negotiate');
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
it('подписанный владелец accept → closed+terminate (приоритет над судьёй)', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: false }, ownerArbitration: 'accept' });
|
||||
expect(r.state).toBe('closed');
|
||||
expect(r.terminate).toBe(true);
|
||||
});
|
||||
it('реальный GO движка → closed+terminate', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' } });
|
||||
expect(r.state).toBe('closed');
|
||||
expect(r.terminate).toBe(true);
|
||||
});
|
||||
it('владелец continue → open', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, ownerArbitration: 'continue' });
|
||||
expect(r.state).toBe('open');
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
it('NO-GO & круг<3 → negotiate', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 1 });
|
||||
expect(r.state).toBe('negotiate');
|
||||
});
|
||||
it('NO-GO & круг>=3 → arbitrate + карточка', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 3 });
|
||||
expect(r.state).toBe('arbitrate');
|
||||
expect(r.card).toBe(true);
|
||||
});
|
||||
it('контроллер без подписи (нет accept, NO-GO) закрыть не может', () => {
|
||||
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 0 });
|
||||
expect(r.terminate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user