Merge branch 'track-c-2b-gate3card'

# Conflicts:
#	tools/enforce-gate3-loop.mjs
This commit is contained in:
Дмитрий
2026-06-17 19:10:52 +03:00
5 changed files with 190 additions and 0 deletions
@@ -0,0 +1,151 @@
# gate3card: судья-карточки (линзы) + видимость стадии — Implementation Plan (v2)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Завести набор линз судьи `gate3card` (сверка пользовательской карточки с продуктом) и чистый помощник видимости `gate3CardSurfaceRecord`, чтобы будущая живая петля приёмки (Сессия E) могла судить карточку и показывать её вердикт в снимке+баннере.
**Architecture:** Две аддитивные правки существующих файлов. (1) В `judge-engine.mjs` добавляется ключ `gate3card` в замороженный `VOTE_LAYOUTS`; `requiredLensesFor` уже generic по любому ключу — отдельной правки логики не требует, проверяется тестом. (2) В `enforce-gate3-loop.mjs` добавляется чистый экспорт `gate3CardSurfaceRecord` — зеркало существующего `gate3SurfaceRecord`, но со стадией `judge:gate3card`. Живая проводка вызова судьи-карточки в петлю — Сессия E (вне этого плана).
**Tech Stack:** Node ESM (`.mjs`), vitest (`vitest.config.tools.mjs`, `--no-file-parallelism`). TDD.
**Кодовая фраза:** роутер-наставник. **Артефакт (опечатан):** gate-3 приёмка владельца v2 §u4 (сверка карточки судьёй) + видимость {#deferred}. **delivery:** internal — управляющая машинерия (линзы судьи + чистый помощник видимости); пользовательского результата владельцу не доставляется, приёмка владельца не требуется.
**Граница (вне объёма, по замечанию наставника на спеку):** честность пометки `delivery` в судье плана (gate-2) и тест неизвестного delivery — **Сессия D**; формат предъявления карточки владельцу в Stop-хуке, таймаут-эскалация, живой вызов судьи-карточки — **Сессия E**. Здесь только линзы + чистый помощник видимости.
**NB по треку B:** `judge-engine.mjs` параллельно правит трек B. Правлю ТОЛЬКО `VOTE_LAYOUTS` (аддитивный ключ), функции не трогаю — слияние позже бесконфликтно.
## Структура файлов
- Изменить (тест): `tools/judge-engine.test.mjs` — тест линз `gate3card` в `requiredLensesFor`.
- Изменить: `tools/judge-engine.mjs` — добавить ключ `gate3card` в `VOTE_LAYOUTS`.
- Изменить (тест): `tools/enforce-gate3-loop.test.mjs` — тесты `gate3CardSurfaceRecord`.
- Изменить: `tools/enforce-gate3-loop.mjs` — добавить чистый `gate3CardSurfaceRecord`.
```skills-json
["test-driven-development"]
```
```steps-json
[
{"op":"Edit","object":"tools/judge-engine.test.mjs","ref":"u4"},
{"op":"Bash","object":"npx vitest run tools/judge-engine.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism","ref":"u4"},
{"op":"Edit","object":"tools/judge-engine.mjs","ref":"u4"},
{"op":"Bash","object":"npx vitest run tools/judge-engine.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism","ref":"u4"},
{"op":"Edit","object":"tools/enforce-gate3-loop.test.mjs","ref":"u4"},
{"op":"Bash","object":"npx vitest run tools/enforce-gate3-loop.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism","ref":"u4"},
{"op":"Edit","object":"tools/enforce-gate3-loop.mjs","ref":"u4"},
{"op":"Bash","object":"npx vitest run tools/enforce-gate3-loop.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism","ref":"u4"},
{"op":"Bash","object":"npx vitest run --reporter dot --config vitest.config.tools.mjs --no-file-parallelism","ref":"u9"}
]
```
```verified-context-json
[{"id":"vc1","kind":"EXTRACTED","ref":"tools/judge-engine.mjs","anchor":"export const VOTE_LAYOUTS"},{"id":"vc2","kind":"EXTRACTED","ref":"tools/enforce-gate3-loop.mjs","anchor":"export function gate3SurfaceRecord"}]
```
---
### Task 1: Линзы `gate3card` в judge-engine (§u4)
- [ ] **Step 1 (Edit `tools/judge-engine.test.mjs`): падающий тест линз**
В блок `describe('requiredLensesFor (§7 раскладка голосов по функциям)', ...)` после теста про риск (перед закрытием `});`) добавить:
```javascript
it('gate3card несёт 3 линзы сверки карточки с продуктом', () => {
const l = requiredLensesFor('gate3card');
expect(l).toEqual(VOTE_LAYOUTS.gate3card);
expect(l).toContain('card_matches_product');
expect(l).toContain('no_overstatement');
expect(l).toContain('verify_steps_real');
});
```
- [ ] **Step 2 (Bash): прогон — тест падает (RED)**
Run: `npx vitest run tools/judge-engine.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism`
Expected: FAIL — `VOTE_LAYOUTS.gate3card` === undefined, `requiredLensesFor('gate3card')` → `[]`. (Под Claude harness-collapse; авторитет — терминал владельца.)
- [ ] **Step 3 (Edit `tools/judge-engine.mjs`): добавить ключ в `VOTE_LAYOUTS`**
После строки `gate3: ['goal_achieved', 'premortem_whole', 'behavior_vs_goal'],` (перед `});`) добавить:
```javascript
gate3card: ['card_matches_product', 'no_overstatement', 'verify_steps_real'],
```
- [ ] **Step 4 (Bash): прогон — тест зелёный (GREEN)**
Run: `npx vitest run tools/judge-engine.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism`
Expected: PASS — тест gate3card + прежние тесты `requiredLensesFor`. (Авторитет — терминал владельца.)
---
### Task 2: Чистый помощник видимости `gate3CardSurfaceRecord` ({#deferred})
- [ ] **Step 5 (Edit `tools/enforce-gate3-loop.test.mjs`): падающие тесты помощника**
После закрытия блока `describe('gate3SurfaceRecord ...')` (перед `describe('loop marker delivery' ...)`) добавить:
```javascript
import { gate3CardSurfaceRecord } from './enforce-gate3-loop.mjs';
describe('gate3CardSurfaceRecord (видимость судьи карточки, стадия judge:gate3card)', () => {
it('GO verdict → stage judge:gate3card, status GO', () => {
expect(gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'GO' }, hash: 'h' }))
.toEqual({ stage: 'judge:gate3card', hash: 'h', status: 'GO', reason: '' });
});
it('NO-GO → status NO-GO, reason дословно', () => {
const r = gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'NO-GO', reason: 'приукрашивание' }, hash: 'h' });
expect(r.status).toBe('NO-GO');
expect(r.reason).toBe('приукрашивание');
});
it('degraded (wired:false) → degraded', () => {
expect(gate3CardSurfaceRecord({ verdict: { wired: false }, hash: 'h' }).status).toBe('degraded');
});
it('нет verdict → skip, hash null', () => {
expect(gate3CardSurfaceRecord({}).status).toBe('skip');
expect(gate3CardSurfaceRecord({}).hash).toBe(null);
});
});
```
- [ ] **Step 6 (Bash): прогон — тесты падают (RED)**
Run: `npx vitest run tools/enforce-gate3-loop.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism`
Expected: FAIL — `gate3CardSurfaceRecord is not a function`. (Авторитет — терминал владельца.)
- [ ] **Step 7 (Edit `tools/enforce-gate3-loop.mjs`): реализация `gate3CardSurfaceRecord`**
После `gate3SurfaceRecord` (перед `runGate3Stop`) добавить:
```javascript
/** Видимость судьи карточки (Фаза 2b): вердикт сверки карточки → стадия judge:gate3card.
* Зеркало gate3SurfaceRecord, тот же канал снимок+баннер (спека видимости {#deferred}). Чистая. */
export function gate3CardSurfaceRecord({ verdict, hash } = {}) {
let status = 'skip';
if (verdict && verdict.wired === false) status = 'degraded';
else if (verdict && verdict.decision === 'GO') status = 'GO';
else if (verdict && verdict.decision === 'NO-GO') status = 'NO-GO';
return { stage: 'judge:gate3card', hash: hash || null, status, reason: (verdict && verdict.reason) || '' };
}
```
- [ ] **Step 8 (Bash): прогон — тесты зелёные (GREEN)**
Run: `npx vitest run tools/enforce-gate3-loop.test.mjs --reporter dot --config vitest.config.tools.mjs --no-file-parallelism`
Expected: PASS — 4 теста gate3CardSurfaceRecord + прежние gate3SurfaceRecord. (Авторитет — терминал владельца.)
- [ ] **Step 9 (Bash): полная регрессия tools**
Run: `npx vitest run --reporter dot --config vitest.config.tools.mjs --no-file-parallelism`
Expected: PASS — база + новые тесты, 0 регрессий (в т.ч. `gate3SurfaceRecord`, `buildJudgePrompt`, `decideGate3Closure`). (Под Claude harness-collapse; авторитетный свод — терминал владельца.)
## Self-Review (план против спеки)
- **§u4 линзы `gate3card`** — Task 1: `card_matches_product`/`no_overstatement`/`verify_steps_real` в `VOTE_LAYOUTS`; `requiredLensesFor` generic, проверен тестом. ✓
- **Видимость {#deferred}** — Task 2: чистый `gate3CardSurfaceRecord` со стадией `judge:gate3card`, зеркало `gate3SurfaceRecord`. Живая запись `writeStage`/`pushVerdict` и сам вызов судьи-карточки — Сессия E. ✓
- **DR-1** — мутирующие шаги 1,3,5,7 сопровождены Bash (2,4,6,8); каждый файл правится ровно ОДИН раз → двух Edit одного файла подряд нет; RED/GREEN — разная неопределённость. ✓
- **Трек B** — правлю только `VOTE_LAYOUTS` (аддитивный ключ), функции judge-engine не трогаю. ✓
- **Обратная совместимость** — оба изменения чисто аддитивные (новый ключ / новый экспорт); существующее поведение не меняется. ✓
- **Вне объёма** — честность delivery на gate-2 + тест неизвестного delivery (Сессия D); показ карточки/таймаут/живой вызов (Сессия E). ✓
+10
View File
@@ -93,6 +93,16 @@ export async function produceGate3Verdict({ judgeKey, callJudge, buildProduct })
}
}
/** Видимость судьи карточки (Фаза 2b): вердикт сверки карточки → стадия judge:gate3card.
* Зеркало gate3SurfaceRecord, тот же канал снимок+баннер (спека видимости {#deferred}). Чистая. */
export function gate3CardSurfaceRecord({ verdict, hash } = {}) {
let status = 'skip';
if (verdict && verdict.wired === false) status = 'degraded';
else if (verdict && verdict.decision === 'GO') status = 'GO';
else if (verdict && verdict.decision === 'NO-GO') status = 'NO-GO';
return { stage: 'judge:gate3card', hash: hash || null, status, reason: (verdict && verdict.reason) || '' };
}
/** Чистая оркестрация хода Stop (deps инъектируются — тест без IO/модели). {block, message?}. */
export async function runGate3Stop(event, deps) {
const { runtimeDir, sess, key, judgeKey, loadGreens, loadArtifact, callJudge, grants, consumed, now } = deps;
+21
View File
@@ -113,6 +113,27 @@ describe('gate3SurfaceRecord (видимость gate3)', () => {
});
});
import { gate3CardSurfaceRecord } from './enforce-gate3-loop.mjs';
describe('gate3CardSurfaceRecord (видимость судьи карточки, стадия judge:gate3card)', () => {
it('GO verdict → stage judge:gate3card, status GO', () => {
expect(gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'GO' }, hash: 'h' }))
.toEqual({ stage: 'judge:gate3card', hash: 'h', status: 'GO', reason: '' });
});
it('NO-GO → status NO-GO, reason дословно', () => {
const r = gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'NO-GO', reason: 'приукрашивание' }, hash: 'h' });
expect(r.status).toBe('NO-GO');
expect(r.reason).toBe('приукрашивание');
});
it('degraded (wired:false) → degraded', () => {
expect(gate3CardSurfaceRecord({ verdict: { wired: false }, hash: 'h' }).status).toBe('degraded');
});
it('нет verdict → skip, hash null', () => {
expect(gate3CardSurfaceRecord({}).status).toBe('skip');
expect(gate3CardSurfaceRecord({}).hash).toBe(null);
});
});
describe('loop marker delivery', () => {
const KEY = 'k-loop-deliv';
it('delivery в подписанной метке верифицируется и ломается при подмене', () => {
+1
View File
@@ -25,6 +25,7 @@ export const VOTE_LAYOUTS = Object.freeze({
part_light: ['correctness', 'simplicity', 'footgun'],
part_risky: ['radius_tests', 'twins', 'attacker'],
gate3: ['goal_achieved', 'premortem_whole', 'behavior_vs_goal'],
gate3card: ['card_matches_product', 'no_overstatement', 'verify_steps_real'],
});
export const PREMORTEM_CLASSES = Object.freeze([
+7
View File
@@ -22,6 +22,13 @@ describe('requiredLensesFor (§7 раскладка голосов по функ
expect(l).toContain('attacker');
expect(l).toContain('money');
});
it('gate3card несёт 3 линзы сверки карточки с продуктом', () => {
const l = requiredLensesFor('gate3card');
expect(l).toEqual(VOTE_LAYOUTS.gate3card);
expect(l).toContain('card_matches_product');
expect(l).toContain('no_overstatement');
expect(l).toContain('verify_steps_real');
});
});
describe('buildJudgePrompt (чистая, детерминированная; слепа к переписке)', () => {