feat: gate-3 owner acceptance live wiring Phase 2d finale
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# gate-3 пользовательская приёмка — оживление в живой петле (Фаза 2d), план r3
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development. Шаги — чекбоксы.
|
||||
|
||||
**Goal:** оживить готовое ядро пользовательской приёмки владельца в живом Stop-хуке
|
||||
`enforce-gate3-loop.mjs`: на завершении плана с пользовательской доставкой и исправном коде собрать
|
||||
карточку, сверить её судьёй карточки, показать владельцу и ждать его подписанного решения.
|
||||
|
||||
**Delivery:** user-result
|
||||
|
||||
**Architecture:** надстройка на существующий хук без новых production-файлов. Добавляются тонкие
|
||||
чистые функции деривации/рендера, расширяется распорядитель зубов входами доставки и вердикта
|
||||
карточки, оживляется оркестрация хода и проводится судья карточки в обвязке.
|
||||
|
||||
**Tech Stack:** Node ESM, vitest, чистые экспортируемые функции (тест без модели/IO).
|
||||
|
||||
## Цель
|
||||
|
||||
Завершить пользовательскую приёмку владельца на gate-3: когда план доводит цель до готового
|
||||
пользовательского результата и судья по коду дал положительный вердикт, хук собирает понятную
|
||||
владельцу карточку (что сделано + как проверить самому + честная граница), сверяет её отдельным
|
||||
судьёй карточки на отсутствие приукрашивания, показывает владельцу с подписанным выбором «принять /
|
||||
вернуть в работу», и закрывает внешнюю петлю только при подписанном согласии владельца. Деградация
|
||||
судьи карточки не вешает конец хода: карточка эскалируется владельцу с явным предупреждением.
|
||||
Результат этого плана — рабочая приёмка, которую владелец видит и принимает (пользовательский
|
||||
результат, не внутренний этап).
|
||||
|
||||
```skills-json
|
||||
["test-driven-development"]
|
||||
```
|
||||
|
||||
```steps-json
|
||||
[
|
||||
{"op": "Edit", "object": "tools/enforce-gate3-loop.test.mjs", "ref": "c6"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t buildOwnerCardFromMarker", "ref": "c6"},
|
||||
{"op": "Edit", "object": "tools/enforce-gate3-loop.mjs", "ref": "c2"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t produceCardVerdict", "ref": "c2"},
|
||||
{"op": "Edit", "object": "tools/enforce-gate3-loop.mjs", "ref": "c3"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t buildOwnerCardFromMarker", "ref": "c6"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t produceCardVerdict", "ref": "c2"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t buildCardJudgeArgs", "ref": "c2"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t renderOwnerCardMessage", "ref": "c1"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism -t decideStopTeeth", "ref": "c3"},
|
||||
{"op": "Edit", "object": "tools/enforce-gate3-loop.mjs", "ref": "c1"},
|
||||
{"op": "Bash", "object": "npx vitest run enforce-gate3-loop --config vitest.config.tools.mjs --no-file-parallelism", "ref": "c1"},
|
||||
{"op": "Edit", "object": "tools/enforce-gate3-loop.mjs", "ref": "c1"},
|
||||
{"op": "Bash", "object": "npx vitest run --config vitest.config.tools.mjs --no-file-parallelism", "ref": "c6"}
|
||||
]
|
||||
```
|
||||
|
||||
```verified-context-json
|
||||
[
|
||||
{"id": "vk1", "kind": "EXTRACTED", "ref": "tools/cost-pricing.mjs", "anchor": "export const PRICING = Object.freeze("},
|
||||
{"id": "vk2", "kind": "EXTRACTED", "ref": "tools/enforce-gate3-loop.mjs", "anchor": "export function decideStopTeeth"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Тесты новых чистых функций (RED)
|
||||
|
||||
**Files:** Test `tools/enforce-gate3-loop.test.mjs`
|
||||
|
||||
- [ ] **Step 1 (Edit):** дописать в конец тест-файла `describe`-блоки: `decideStopTeeth` с
|
||||
`delivery`+`cardVerdict` (internal→closed; user-result+карточка NO-GO→await-card; +карточка
|
||||
GO→await-owner unverified:false; +карточка degraded→await-owner unverified:true; +нет
|
||||
карточки→await-card; +подписанный accept→closed), `buildOwnerCardFromMarker` (цель из секций,
|
||||
verifySteps из GREEN, честные заглушки change/boundary, machinery, honestyChecked:false→warning),
|
||||
`produceCardVerdict` (нет ключа→degraded; заход бросил→degraded+cause; вердикт→проброс),
|
||||
`buildCardJudgeArgs` (факты+карточка+цель), `renderOwnerCardMessage` (accept/continue по
|
||||
отпечатку; unverified→предупреждение).
|
||||
- [ ] **Step 2 (Bash):** `... -t buildOwnerCardFromMarker` — FAIL (функции не объявлены).
|
||||
|
||||
### Task 2: Импорт сборщика карточки
|
||||
|
||||
**Files:** Modify `tools/enforce-gate3-loop.mjs` (строка импорта из `./loop-termination.mjs`)
|
||||
|
||||
- [ ] **Step 3 (Edit):** добавить `buildOwnerCard` в импорт из `./loop-termination.mjs`.
|
||||
- [ ] **Step 4 (Bash):** `... -t produceCardVerdict` (RED — производитель ещё не объявлен).
|
||||
|
||||
### Task 3: Чистые функции + расширение decideStopTeeth
|
||||
|
||||
**Files:** Modify `tools/enforce-gate3-loop.mjs` (функция `decideStopTeeth` + вставка ниже неё)
|
||||
|
||||
- [ ] **Step 5 (Edit):** расширить `decideStopTeeth` входами `delivery`/`cardVerdict` (прокинуть в
|
||||
`decideGate3Closure`, добавить `unverified` в блок-ветку) и вставить ниже четыре чистые функции:
|
||||
`buildOwnerCardFromMarker`, `produceCardVerdict`, `buildCardJudgeArgs`, `renderOwnerCardMessage`.
|
||||
- [ ] **Step 6 (Bash):** `... -t buildOwnerCardFromMarker` — GREEN (подтверждение прохождения).
|
||||
- [ ] **Step 7 (Bash):** `... -t produceCardVerdict` — GREEN.
|
||||
- [ ] **Step 8 (Bash):** `... -t buildCardJudgeArgs` — GREEN.
|
||||
- [ ] **Step 9 (Bash):** `... -t renderOwnerCardMessage` — GREEN.
|
||||
- [ ] **Step 10 (Bash):** `... -t decideStopTeeth` — GREEN (новые ветки delivery/cardVerdict).
|
||||
|
||||
### Task 4: Оживление runGate3Stop
|
||||
|
||||
**Files:** Modify `tools/enforce-gate3-loop.mjs` (функция `runGate3Stop`)
|
||||
|
||||
- [ ] **Step 11 (Edit):** на код-GO + пользовательская доставка собрать карточку
|
||||
(`buildOwnerCardFromMarker`), сверить судьёй карточки (`produceCardVerdict` через инъектируемый
|
||||
`callCardJudge`), при GO пометить `honestyChecked:true`, записать видимость стадии
|
||||
`judge:gate3card`, прокинуть `delivery`+`cardVerdict` в `decideStopTeeth`, кэшировать
|
||||
`cardVerdict`+`card`, рендерить показ владельцу (`await-owner`) и доработку (`await-card`).
|
||||
Локальную переменную карточки арбитража переименовать, чтобы не затеняла внешнюю карточку.
|
||||
- [ ] **Step 12 (Bash):** `npx vitest run enforce-gate3-loop ... --no-file-parallelism` — весь
|
||||
файл GREEN (парсится + все чистые тесты проходят).
|
||||
|
||||
### Task 5: Проводка судьи карточки в main()
|
||||
|
||||
**Files:** Modify `tools/enforce-gate3-loop.mjs` (функция `main`)
|
||||
|
||||
- [ ] **Step 13 (Edit):** добавить `callCardJudge` (functionName `gate3card`, линзы
|
||||
`requiredLensesFor('gate3card')`, тот же `callJudgeModel`/`runJudge`) и передать его в deps
|
||||
`runGate3Stop`.
|
||||
- [ ] **Step 14 (Bash):** `npx vitest run --config vitest.config.tools.mjs --no-file-parallelism`
|
||||
— полная регрессия, ноль регрессий.
|
||||
@@ -0,0 +1,100 @@
|
||||
# gate-3 пользовательская приёмка — оживление в живой петле (Фаза 2d)
|
||||
|
||||
**Дата:** 2026-06-17
|
||||
**Эпик:** роутер-наставник, E-S1 (приёмка владельца), завершающая фаза.
|
||||
**Предшественники:** ядро замыкания, сборщик карточки и линзы судьи карточки уже готовы; здесь они
|
||||
прокидываются в живую оркестрацию Stop-хука.
|
||||
|
||||
## Цель
|
||||
|
||||
Завершить пользовательскую приёмку владельца на gate-3. Когда план доводит цель до готового
|
||||
пользовательского результата и судья по коду подтвердил исправность, живой Stop-хук должен собрать
|
||||
понятную владельцу карточку (что сделано + как проверить самому + честная граница), сверить её
|
||||
отдельным судьёй карточки на отсутствие приукрашивания, показать владельцу с подписанным выбором
|
||||
«принять / вернуть в работу» и закрыть внешнюю петлю только при подписанном согласии владельца.
|
||||
Деградация судьи карточки не вешает конец хода: карточка эскалируется владельцу с явным
|
||||
предупреждением «сверка недоступна». Внутренние планы закрываются по судье кода и владельца не
|
||||
беспокоят.
|
||||
|
||||
## Контракт живой оркестрации {#c1}
|
||||
|
||||
В живом Stop-хуке после получения вердикта судьи по коду:
|
||||
|
||||
- режим карточки определяется пометкой плана из подписанной метки: пользовательский результат →
|
||||
ветка карточки; внутренний → закрытие по судье кода без карточки;
|
||||
- ветка карточки активируется только при исправном коде (положительный вердикт судьи по коду);
|
||||
при отрицательном/недоступном судье кода работает прежняя логика переговоров/арбитража;
|
||||
- решение о блокировке конца хода принимает чистый детерминированный распорядитель, получающий
|
||||
пометку доставки и вердикт карточки дополнительными входами;
|
||||
- показ карточки владельцу несёт подписанный выбор по отпечатку (принять / вернуть в работу);
|
||||
закрытие петли — только по подписанному согласию владельца, распорядитель сам не закрывает.
|
||||
|
||||
## Сборка и сверка карточки {#c2}
|
||||
|
||||
- **Сборка** — чистая деривация полей карточки из подписанной метки, опечатанного артефакта и
|
||||
по-критерийных подтверждений: цель — из секций артефакта; шаги проверки — реальные воспроизводимые
|
||||
по-критерию подтверждения (владелец перепроверяет сам); поля, для которых структурного источника
|
||||
нет, заполняются честными заглушками, а не выдумкой. Путь машинерии (без экранного UI).
|
||||
- **Сверка** — отдельный звонок судьи карточки набором линз «карточка соответствует продукту /
|
||||
нет заявлений сверх подтверждённого / шаги проверки реальны». Производитель вердикта зеркалит
|
||||
производителя вердикта судьи кода: отсутствие ключа или срыв захода дают видимую деградацию с
|
||||
причиной, а не молчаливый проброс наверх.
|
||||
- Положительная сверка помечает карточку проверенной (снимает предупреждение); отрицательная
|
||||
содержательная сверка отправляет карточку на доработку без вызова владельца; деградация сверки
|
||||
эскалирует карточку владельцу с предупреждением «честность не подтверждена автоматически».
|
||||
|
||||
## Замыкание и состояния {#c3}
|
||||
|
||||
Распорядитель замыкания (уже существующий) при пользовательской доставке и исправном коде даёт:
|
||||
|
||||
- карточка содержательно не пройдена → состояние «дорабатывать карточку», владельца не зовём, блок;
|
||||
- карточка проверена, подписи владельца нет → «ждать владельца», показываем проверенную карточку, блок;
|
||||
- карточка недоступна для сверки (деградация), подписи нет → «ждать владельца» с под-флагом
|
||||
«непроверено», показываем карточку с предупреждением, блок (живучесть сохранена);
|
||||
- подписанное согласие владельца → закрытие (приоритетно);
|
||||
- внутренняя доставка + исправный код → закрытие по судье кода.
|
||||
|
||||
Закрытие достижимо только через распорядитель завершения; ни деградация судьи кода, ни деградация
|
||||
судьи карточки сами петлю не закрывают.
|
||||
|
||||
## Крайние случаи {#c4}
|
||||
|
||||
- Пустые источники полей карточки → честные заглушки, не выдумки; карточка с непроверенной
|
||||
честностью всегда несёт видимое предупреждение.
|
||||
- Повторный конец хода с тем же отпечатком переиспользует закэшированный вердикт карточки (без
|
||||
повторного звонка судье); смена отпечатка пересобирает карточку и вердикт заново.
|
||||
- Локальная переменная карточки арбитража не должна затенять внешнюю переменную карточки приёмки.
|
||||
- Видимость стадии сверки карточки пишется отдельным каналом снимка/баннера (fail-quiet, на зубы
|
||||
петли не влияет).
|
||||
|
||||
## Конвенция {#c5}
|
||||
|
||||
Чистые экспортируемые функции (деривация карточки, производитель вердикта карточки, сборка
|
||||
аргументов судье, рендер показа владельцу, расширение распорядителя зубов) тестируемы в изоляции
|
||||
без модели и без ввода-вывода. Без новых внешних зависимостей и без новых production-файлов:
|
||||
надстройка укладывается в существующий хук. Пометка доставки — строковое поле из фиксированного
|
||||
множества; неизвестное значение трактуется как внутренняя доставка.
|
||||
|
||||
## Критерий приёмки {#c6}
|
||||
|
||||
TDD-тесты:
|
||||
|
||||
- распорядитель зубов с входами доставки и вердикта карточки даёт корректные состояния
|
||||
(внутренняя+код-GO → закрыть; пользовательская+код-GO+карточка-NO-GO → дорабатывать без владельца;
|
||||
+карточка-GO → ждать владельца; +карточка-деградация → ждать владельца с флагом «непроверено»;
|
||||
+нет карточки → дорабатывать; +подписанное согласие → закрыть);
|
||||
- деривация карточки: цель из секций, шаги проверки из подтверждений, честные заглушки пустых полей,
|
||||
путь машинерии, непроверенная честность → предупреждение;
|
||||
- производитель вердикта карточки: нет ключа → деградация; срыв захода → видимая деградация с
|
||||
причиной; вердикт → проброс без искажения;
|
||||
- сборка аргументов судье: подтверждённые факты продукта + сериализованная карточка + цель;
|
||||
- рендер показа владельцу: подписанный выбор принять/вернуть по отпечатку; флаг «непроверено» →
|
||||
видимое предупреждение;
|
||||
- полная регрессия свода зелёная, ноль регрессий в переиспользуемых модулях.
|
||||
|
||||
```verified-context-json
|
||||
[
|
||||
{"id": "ek1", "kind": "EXTRACTED", "ref": "tools/loop-termination.mjs", "anchor": "decideGate3Closure"},
|
||||
{"id": "ek2", "kind": "EXTRACTED", "ref": "tools/cost-pricing.mjs", "anchor": "export const PRICING = Object.freeze("}
|
||||
]
|
||||
```
|
||||
+106
-13
@@ -9,7 +9,7 @@ import fsDefault from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { canonicalJson, signPayload, verifyReceipt } from './receipt-sign.mjs';
|
||||
import { buildGate3Product, decideGate3Closure } from './loop-termination.mjs';
|
||||
import { buildGate3Product, decideGate3Closure, buildOwnerCard } from './loop-termination.mjs';
|
||||
import { escapeGrantOpen } from './escape-grant.mjs';
|
||||
import { parseNegotiationSection } from './negotiation-section.mjs';
|
||||
import { buildArbitrationCard } from './arbitration-card.mjs';
|
||||
@@ -62,11 +62,74 @@ export function resolveOwnerArbitration({ fingerprint, grants, consumed, now })
|
||||
return null;
|
||||
}
|
||||
|
||||
export function decideStopTeeth({ verdict, noGoCount = 0, ownerArbitration = null, maxRounds = ESCALATE_AFTER }) {
|
||||
const d = decideGate3Closure({ gate3Verdict: verdict, noGoCount, ownerArbitration, maxRounds });
|
||||
export function decideStopTeeth({ verdict, noGoCount = 0, ownerArbitration = null, maxRounds = ESCALATE_AFTER, delivery = 'internal', cardVerdict = null }) {
|
||||
const d = decideGate3Closure({ gate3Verdict: verdict, noGoCount, ownerArbitration, maxRounds, delivery, cardVerdict });
|
||||
if (d.state === 'closed') return { block: false, clear: true, state: d.state, reason: d.reason };
|
||||
if (d.state === 'open') return { block: false, clear: false, state: d.state, reason: d.reason };
|
||||
return { block: true, clear: false, state: d.state, card: !!d.card, reason: d.reason };
|
||||
return { block: true, clear: false, state: d.state, card: !!d.card, unverified: !!d.unverified, reason: d.reason };
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 Фаза 2d §c2: сборка пользовательской карточки из подписанной метки + опечатанного артефакта
|
||||
* + по-критерийных GREEN. Честная деривация: цель из секций артефакта; verifySteps — реальные
|
||||
* по-критерию GREEN (владелец воспроизводит сам); change/boundary — честные заглушки (поведенческое
|
||||
* изменение машинерии структурно не извлекается; не выдумываем). Путь machinery (claude-brain без UI).
|
||||
* honestyChecked=false до GO судьи карточки. Чистая.
|
||||
*/
|
||||
export function buildOwnerCardFromMarker({ marker, frozenArtifact, greens } = {}) {
|
||||
const sections = (frozenArtifact && frozenArtifact.sections) || {};
|
||||
const goal = Object.keys(sections).sort().map((k) => `[${k}] ${sections[k]}`).join('\n');
|
||||
const verifySteps = (Array.isArray(greens) ? greens : []).filter((g) => g && g.green).map((g) => `критерий ${g.criterion_id}: GREEN (воспроизводимо прогоном свода)`);
|
||||
return buildOwnerCard({ goal, change: [], verifySteps, boundary: '', kind: 'machinery', honestyChecked: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 Фаза 2d §c2: производитель вердикта судьи карточки. Зеркало produceGate3Verdict — нет
|
||||
* ключа/захода → degraded (wired:false); срыв захода → ВИДИМЫЙ degraded с cause (не молчит, не
|
||||
* fail-open наверх). Чистая (buildArgs инъектируется).
|
||||
*/
|
||||
export async function produceCardVerdict({ judgeKey, callCardJudge, buildArgs } = {}) {
|
||||
if (!(judgeKey && callCardJudge)) return { wired: false, decision: 'GO', unavailable: true };
|
||||
try {
|
||||
return await callCardJudge(buildArgs());
|
||||
} catch (e) {
|
||||
return { wired: false, decision: 'GO', unavailable: true, cause: `судья карточки сорвался: ${String((e && e.message) || e).slice(0, 200)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 Фаза 2d §c2: аргументы судье карточки — подтверждённые факты продукта + сериализованная
|
||||
* карточка простого языка. Судья сверяет card_matches_product/no_overstatement/verify_steps_real. Чистая.
|
||||
*/
|
||||
export function buildCardJudgeArgs({ card, gate3Product } = {}) {
|
||||
const facts = (gate3Product && gate3Product.product) || '';
|
||||
const goal = (gate3Product && gate3Product.goal) || '';
|
||||
const c = card || {};
|
||||
const cardText = [
|
||||
`цель: ${c.goal || ''}`,
|
||||
`что изменилось: ${(Array.isArray(c.change) ? c.change : []).join(' | ')}`,
|
||||
`как проверить: ${(Array.isArray(c.verifySteps) ? c.verifySteps : []).join(' | ')}`,
|
||||
`граница: ${c.boundary || ''}`,
|
||||
].join('\n');
|
||||
return { product: `ПОДТВЕРЖДЁННЫЕ ФАКТЫ ПРОДУКТА:\n${facts}\n\nКАРТОЧКА ВЛАДЕЛЬЦУ:\n${cardText}`, goal, cards: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* E-S1 Фаза 2d §c1: рендер показа карточки владельцу на конце хода с подписанным выбором
|
||||
* accept/continue по отпечатку. unverified (degraded судья карточки) → видимое предупреждение. Чистая.
|
||||
*/
|
||||
export function renderOwnerCardMessage({ card, fingerprint, unverified = false } = {}) {
|
||||
const c = card || {};
|
||||
const lines = ['Приёмка результата владельцем — цель доведена до пользовательского результата.'];
|
||||
if (unverified || c.honestyChecked !== true) {
|
||||
lines.push(`⚠ ${c.warning || 'автоматическая сверка честности недоступна — проверь по шагам сам'}`);
|
||||
}
|
||||
lines.push(`Цель: ${c.goal || '(не указана)'}`);
|
||||
lines.push(`Что изменилось: ${(Array.isArray(c.change) ? c.change : []).join('; ') || '(не указано)'}`);
|
||||
lines.push(`Как проверить самому: ${(Array.isArray(c.verifySteps) ? c.verifySteps : []).join('; ') || '(не указано)'}`);
|
||||
lines.push(`Чего НЕ делает: ${c.boundary || '(не указано)'}`);
|
||||
lines.push(`Выбор владельца: FLOOR-ESCAPE: gate3-arb:accept:${fingerprint} (принять как достигнутое) либо gate3-arb:continue:${fingerprint} (вернуть в работу).`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Видимость gate3: вердикт судьи цели → запись стадии снимка/баннера. Чистая (без import). */
|
||||
@@ -105,25 +168,43 @@ export function gate3CardSurfaceRecord({ verdict, hash } = {}) {
|
||||
|
||||
/** Чистая оркестрация хода Stop (deps инъектируются — тест без IO/модели). {block, message?}. */
|
||||
export async function runGate3Stop(event, deps) {
|
||||
const { runtimeDir, sess, key, judgeKey, loadGreens, loadArtifact, callJudge, grants, consumed, now } = deps;
|
||||
const { runtimeDir, sess, key, judgeKey, loadGreens, loadArtifact, callJudge, callCardJudge, grants, consumed, now } = deps;
|
||||
const marker = readLoopOpen({ runtimeDir, sess, key });
|
||||
if (!marker) return { block: false };
|
||||
const delivery = marker.delivery === 'user-result' ? 'user-result' : 'internal';
|
||||
const greens = (loadGreens && loadGreens()) || [];
|
||||
const greenIds = greens.filter((g) => g && g.green).map((g) => g.criterion_id);
|
||||
const frozenArtifact = (loadArtifact && loadArtifact()) || null;
|
||||
const negotiationText = parseNegotiationSection((frozenArtifact && frozenArtifact._md) || '').map((r) => r.position).join('\n');
|
||||
const fingerprint = computeFingerprint({ planId: marker.planId, greenIds, negotiationText });
|
||||
const cache = loadCache({ runtimeDir, sess }) || { fingerprint: null, verdict: null, noGoCount: 0 };
|
||||
const cache = loadCache({ runtimeDir, sess }) || { fingerprint: null, verdict: null, noGoCount: 0, cardVerdict: null, card: null };
|
||||
|
||||
let verdict = cache.verdict;
|
||||
let noGoCount = cache.noGoCount || 0;
|
||||
let cardVerdict = cache.cardVerdict || null;
|
||||
let card = cache.card || null;
|
||||
if (cache.fingerprint !== fingerprint) {
|
||||
// Срыв построения продукта/захода → видимый degraded (не немой fail-open в main()).
|
||||
verdict = await produceGate3Verdict({ judgeKey, callJudge, buildProduct: () => buildGate3ProductFromMarker({ marker, frozenArtifact, greens }) });
|
||||
const isContentNoGo = !!verdict && verdict.wired === true && verdict.decision !== 'GO';
|
||||
const isContentGo = !!verdict && verdict.wired === true && verdict.decision === 'GO';
|
||||
noGoCount = isContentNoGo ? noGoCount + 1 : (isContentGo ? 0 : noGoCount);
|
||||
if (verdict && verdict.wired !== false) saveCache({ runtimeDir, sess, cache: { fingerprint, verdict, noGoCount } });
|
||||
// §c1/§c2: код исправен + план доводит до пользовательского результата → собрать карточку и сверить судьёй карточки.
|
||||
cardVerdict = null; card = null;
|
||||
const codeGo = !!verdict && verdict.decision === 'GO' && verdict.wired !== false;
|
||||
if (codeGo && delivery === 'user-result') {
|
||||
card = buildOwnerCardFromMarker({ marker, frozenArtifact, greens });
|
||||
cardVerdict = await produceCardVerdict({ judgeKey, callCardJudge, buildArgs: () => buildCardJudgeArgs({ card, gate3Product: buildGate3ProductFromMarker({ marker, frozenArtifact, greens }) }) });
|
||||
if (cardVerdict && cardVerdict.decision === 'GO' && cardVerdict.wired !== false) { const { warning, ...rest } = card; card = { ...rest, honestyChecked: true }; }
|
||||
// Видимость судьи карточки → снимок + баннер (fail-quiet, на зубы не влияет).
|
||||
try {
|
||||
const { writeStage, pushVerdict } = await import('./verdict-surface-store.mjs');
|
||||
const rec = gate3CardSurfaceRecord({ verdict: cardVerdict, hash: marker.planId });
|
||||
writeStage(sess, { ...rec, ts: Date.now() }, runtimeDir);
|
||||
pushVerdict(sess, { outcome: rec.status, gate: 'judge:gate3card', round: null, version: null, reason: rec.reason }, runtimeDir);
|
||||
} catch { /* fail-quiet */ }
|
||||
}
|
||||
if (verdict && verdict.wired !== false) saveCache({ runtimeDir, sess, cache: { fingerprint, verdict, noGoCount, cardVerdict, card } });
|
||||
// Видимость «всё в лоб»: свежий gate3-вердикт → снимок + баннер (fail-quiet, не влияет на зубы).
|
||||
try {
|
||||
const { writeStage, pushVerdict } = await import('./verdict-surface-store.mjs');
|
||||
@@ -134,14 +215,18 @@ export async function runGate3Stop(event, deps) {
|
||||
}
|
||||
|
||||
const ownerArbitration = resolveOwnerArbitration({ fingerprint, grants, consumed, now });
|
||||
const t = decideStopTeeth({ verdict, noGoCount, ownerArbitration });
|
||||
if (t.clear) { clearLoopOpen({ runtimeDir, sess }); saveCache({ runtimeDir, sess, cache: { fingerprint: null, verdict: null, noGoCount: 0 } }); }
|
||||
const t = decideStopTeeth({ verdict, noGoCount, ownerArbitration, delivery, cardVerdict });
|
||||
if (t.clear) { clearLoopOpen({ runtimeDir, sess }); saveCache({ runtimeDir, sess, cache: { fingerprint: null, verdict: null, noGoCount: 0, cardVerdict: null, card: null } }); }
|
||||
if (!t.block) return { block: false };
|
||||
|
||||
let message;
|
||||
if (t.state === 'arbitrate') {
|
||||
const card = buildArbitrationCard({ side: 'judge', level: 'L2', round: noGoCount, objectionVerbatim: t.reason || '(возражение судьи)', controllerPositionVerbatim: negotiationText || '(позиция не указана)', sealAction: `gate3-arb:accept:${fingerprint}` });
|
||||
message = `[gate3-loop] ${card.title}\nЦель не подтверждена. Замечание: ${card.objection}\nВыбор владельца: FLOOR-ESCAPE: gate3-arb:accept:${fingerprint} (принять) / gate3-arb:continue:${fingerprint} (продолжать).`;
|
||||
if (t.state === 'await-owner') {
|
||||
message = renderOwnerCardMessage({ card, fingerprint, unverified: !!t.unverified });
|
||||
} else if (t.state === 'await-card') {
|
||||
message = buildObjectionFeedback({ side: 'judge', text: t.reason || 'карточка приукрашена/неточна — доработай карточку; владельца не зовём' });
|
||||
} else if (t.state === 'arbitrate') {
|
||||
const cardArb = buildArbitrationCard({ side: 'judge', level: 'L2', round: noGoCount, objectionVerbatim: t.reason || '(возражение судьи)', controllerPositionVerbatim: negotiationText || '(позиция не указана)', sealAction: `gate3-arb:accept:${fingerprint}` });
|
||||
message = `[gate3-loop] ${cardArb.title}\nЦель не подтверждена. Замечание: ${cardArb.objection}\nВыбор владельца: FLOOR-ESCAPE: gate3-arb:accept:${fingerprint} (принять) / gate3-arb:continue:${fingerprint} (продолжать).`;
|
||||
} else if (verdict && verdict.wired === false) {
|
||||
message = buildDegradedFeedback({ side: 'judge', reason: 'судья gate-3 недоступен — петля не закрыта; выход: escape владельца ИЛИ plan-done' });
|
||||
} else {
|
||||
@@ -173,7 +258,15 @@ async function main() {
|
||||
const v = runJudge({ functionName: 'gate3', requiredLenses, subRunsRequired: [], subRuns: [], llmCall: () => raw, promptArgs });
|
||||
return { wired: true, decision: v.decision, verdict: v };
|
||||
};
|
||||
const r = await runGate3Stop(event, { runtimeDir: dir, sess, key, judgeKey, loadGreens, loadArtifact, callJudge, grants: loadFloorEscapes(sess), consumed: loadConsumed(sess), now: Date.now() });
|
||||
const callCardJudge = async (args) => {
|
||||
const requiredLenses = requiredLensesFor('gate3card');
|
||||
const promptArgs = { ...args, roundMemory: {} };
|
||||
const raw = await callJudgeModel({ functionName: 'gate3card', requiredLenses, promptArgs, apiKey: judgeKey });
|
||||
if (raw && raw.unavailable) return { wired: false, decision: 'GO', unavailable: true };
|
||||
const v = runJudge({ functionName: 'gate3card', requiredLenses, subRunsRequired: [], subRuns: [], llmCall: () => raw, promptArgs });
|
||||
return { wired: true, decision: v.decision, verdict: v };
|
||||
};
|
||||
const r = await runGate3Stop(event, { runtimeDir: dir, sess, key, judgeKey, loadGreens, loadArtifact, callJudge, callCardJudge, grants: loadFloorEscapes(sess), consumed: loadConsumed(sess), now: Date.now() });
|
||||
exitDecision({ block: !!r.block, message: r.block ? `[gate3-loop] ${r.message || 'петля открыта — цель не подтверждена'}` : undefined });
|
||||
} catch {
|
||||
exitDecision({ block: false }); // Stop fail-OPEN: внутренняя ошибка хука НЕ кирпичит конец хода
|
||||
|
||||
@@ -170,3 +170,94 @@ describe('produceGate3Verdict (видимость срыва gate3 — фикс
|
||||
expect(r).toEqual(v);
|
||||
});
|
||||
});
|
||||
|
||||
import { buildOwnerCardFromMarker, produceCardVerdict, buildCardJudgeArgs, renderOwnerCardMessage } from './enforce-gate3-loop.mjs';
|
||||
|
||||
describe('decideStopTeeth — delivery + cardVerdict (Фаза 2d приёмка)', () => {
|
||||
const go = { wired: true, decision: 'GO' };
|
||||
const cardGo = { wired: true, decision: 'GO' };
|
||||
const cardNoGo = { wired: true, decision: 'NO-GO', reason: 'приукрашено' };
|
||||
const cardDegraded = { wired: false, decision: 'GO' };
|
||||
it('internal + код-GO → allow + clear (closed)', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'internal' });
|
||||
expect(r.block).toBe(false); expect(r.clear).toBe(true);
|
||||
});
|
||||
it('user-result + код-GO + карточка NO-GO → block await-card (владельца не зовём)', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardNoGo });
|
||||
expect(r.block).toBe(true); expect(r.state).toBe('await-card');
|
||||
});
|
||||
it('user-result + код-GO + карточка GO → block await-owner, unverified false', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardGo });
|
||||
expect(r.block).toBe(true); expect(r.state).toBe('await-owner'); expect(r.unverified).toBe(false);
|
||||
});
|
||||
it('user-result + код-GO + карточка degraded → block await-owner unverified:true', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardDegraded });
|
||||
expect(r.block).toBe(true); expect(r.state).toBe('await-owner'); expect(r.unverified).toBe(true);
|
||||
});
|
||||
it('user-result + код-GO + карточка ещё не сверена → await-card', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: null });
|
||||
expect(r.block).toBe(true); expect(r.state).toBe('await-card');
|
||||
});
|
||||
it('user-result + подписанный accept → allow + clear (closed)', () => {
|
||||
const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardGo, ownerArbitration: 'accept' });
|
||||
expect(r.block).toBe(false); expect(r.clear).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildOwnerCardFromMarker (Фаза 2d сборка карточки)', () => {
|
||||
it('цель из секций + verifySteps из GREEN + честные заглушки + machinery + warning', () => {
|
||||
const marker = { steps: [], planId: 'p', delivery: 'user-result' };
|
||||
const frozenArtifact = { sections: { s1: 'дать владельцу приёмку' } };
|
||||
const card = buildOwnerCardFromMarker({ marker, frozenArtifact, greens: [{ criterion_id: 'c1', green: true }, { criterion_id: 'c2', green: false }] });
|
||||
expect(card.goal).toContain('дать владельцу приёмку');
|
||||
expect(card.verifySteps.join(' ')).toContain('c1');
|
||||
expect(card.verifySteps.join(' ')).not.toContain('c2');
|
||||
expect(card.kind).toBe('machinery');
|
||||
expect(card.honestyChecked).toBe(false);
|
||||
expect(card.warning).toBeTruthy();
|
||||
});
|
||||
it('пустые входы → честные заглушки, не выдумки', () => {
|
||||
const card = buildOwnerCardFromMarker({ marker: { steps: [] }, frozenArtifact: null, greens: [] });
|
||||
expect(card.goal).toBe('(цель не указана)');
|
||||
expect(card.change).toEqual(['(что изменилось — не указано)']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('produceCardVerdict (видимость срыва судьи карточки)', () => {
|
||||
it('нет ключа → degraded', async () => {
|
||||
const r = await produceCardVerdict({ judgeKey: null, callCardJudge: async () => ({ wired: true, decision: 'GO' }), buildArgs: () => ({}) });
|
||||
expect(r.wired).toBe(false); expect(r.unavailable).toBe(true);
|
||||
});
|
||||
it('заход бросил → видимый degraded с cause', async () => {
|
||||
const r = await produceCardVerdict({ judgeKey: 'K', callCardJudge: async () => { throw new Error('boom'); }, buildArgs: () => ({}) });
|
||||
expect(r.wired).toBe(false); expect(typeof r.cause).toBe('string'); expect(r.cause.length).toBeGreaterThan(0);
|
||||
});
|
||||
it('вердикт → проброс', async () => {
|
||||
const v = { wired: true, decision: 'NO-GO', reason: 'overstatement' };
|
||||
const r = await produceCardVerdict({ judgeKey: 'K', callCardJudge: async () => v, buildArgs: () => ({}) });
|
||||
expect(r).toEqual(v);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCardJudgeArgs', () => {
|
||||
it('факты продукта + сериализованная карточка + цель', () => {
|
||||
const args = buildCardJudgeArgs({ card: { goal: 'G', change: ['c'], verifySteps: ['v'], boundary: 'b' }, gate3Product: { product: 'ФАКТЫ', goal: 'G' } });
|
||||
expect(args.product).toContain('ФАКТЫ');
|
||||
expect(args.product).toContain('как проверить');
|
||||
expect(args.product).toContain('v');
|
||||
expect(args.goal).toBe('G');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderOwnerCardMessage', () => {
|
||||
it('содержит accept/continue по отпечатку + поля карточки', () => {
|
||||
const msg = renderOwnerCardMessage({ card: { goal: 'G', change: ['ch'], verifySteps: ['st'], boundary: 'bd', honestyChecked: true }, fingerprint: 'FP' });
|
||||
expect(msg).toContain('gate3-arb:accept:FP');
|
||||
expect(msg).toContain('gate3-arb:continue:FP');
|
||||
expect(msg).toContain('st');
|
||||
});
|
||||
it('unverified → видимое предупреждение', () => {
|
||||
const msg = renderOwnerCardMessage({ card: { goal: 'G', warning: 'не проверено' }, fingerprint: 'FP', unverified: true });
|
||||
expect(msg).toContain('⚠');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user