feat: E-S1 2c chestnost delivery na gate-2 - linza delivery_honesty

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-17 18:01:24 +03:00
parent ba584a8335
commit 451b0d4cf5
4 changed files with 57 additions and 7 deletions
+3 -1
View File
@@ -131,7 +131,9 @@ export async function runJudgeGate(event, deps = {}) {
if (typeof deps.roundMemoryImpl === 'function') {
try { roundMemory = (await deps.roundMemoryImpl({ stage, content: rmContent })) || {}; } catch { roundMemory = {}; }
}
const promptArgs = { product: g.product, goal: g.goal, cards: g.cards, roundMemory };
let delivery = null;
if (functionName === 'gate2') { try { delivery = sealablePlan(rmContent).delivery; } catch { delivery = 'internal'; } }
const promptArgs = { product: g.product, goal: g.goal, cards: g.cards, roundMemory, delivery };
const raw = await callJudgeModel({ functionName, requiredLenses, promptArgs, apiKey, model: deps.model, transport: deps.transport });
if (raw && raw.unavailable) {
// M7: причина недоступности протекает в вердикт → лог-WARN + seal-запись её фиксируют.
+11 -2
View File
@@ -43,8 +43,17 @@ describe('bindingHashForJudge (Фаза 3 — какой хеш судья св
});
});
const okText = JSON.stringify({ slots: { spec_fidelity: 'aaaaaaaa', verifiability: 'bbbbbbbb', plan_soundness: 'cccccccc', execution_risk: 'dddddddd', step_clarity: 'eeeeeeee' }, objections: [] });
const noText = JSON.stringify({ slots: { spec_fidelity: 'aaaaaaaa', verifiability: 'bbbbbbbb', plan_soundness: 'cccccccc', execution_risk: 'dddddddd', step_clarity: 'eeeeeeee' }, objections: [{ verdict: 'NO', anchor: { kind: 'spec_section', ref: '§1' }, severity: 'heavy', reversible: false }] });
const okText = JSON.stringify({ slots: { spec_fidelity: 'aaaaaaaa', verifiability: 'bbbbbbbb', plan_soundness: 'cccccccc', execution_risk: 'dddddddd', step_clarity: 'eeeeeeee', delivery_honesty: 'ffffffff' }, objections: [] });
const noText = JSON.stringify({ slots: { spec_fidelity: 'aaaaaaaa', verifiability: 'bbbbbbbb', plan_soundness: 'cccccccc', execution_risk: 'dddddddd', step_clarity: 'eeeeeeee', delivery_honesty: 'ffffffff' }, objections: [{ verdict: 'NO', anchor: { kind: 'spec_section', ref: '§1' }, severity: 'heavy', reversible: false }] });
describe('runJudgeGate — delivery в промпте судьи', () => {
it('gate2 прокидывает пометку delivery в user-промпт', async () => {
const prompts = [];
const ev = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/x.md', content: '# П\n**Delivery:** user-result\n## Цель\nцель\n```steps-json\n[{"op":"Edit","object":"tools/x.mjs","ref":"d1"}]\n```' } };
await runJudgeGate(ev, { judgeActiveImpl: () => true, apiKey: 'K', transport: async (p) => { prompts.push(p); return okText; } });
expect(prompts[0].user).toContain('ПОМЕТКА DELIVERY: user-result');
});
});
const planEv = () => ({ tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/x.md', content: '# П\n## Цель\nцель плана тут\nшаг' } });
describe('enforce-judge-gate decide (М7 Фаза 7 §8) — mode-aware + finalGate', () => {
+9 -4
View File
@@ -19,7 +19,7 @@ import { requiredSubRunsPresent } from './judge-subrun-journal.mjs';
export const VOTE_LAYOUTS = Object.freeze({
gate1: ['completeness', 'premortem', 'goal_advocate', 'correctness'], // премортем = K7
gate2: ['spec_fidelity', 'verifiability', 'plan_soundness', 'execution_risk', 'step_clarity'],
gate2: ['spec_fidelity', 'verifiability', 'plan_soundness', 'execution_risk', 'step_clarity', 'delivery_honesty'],
a2_divergence: ['intent_fidelity'],
a2_destructive: ['radius_reversibility', 'attacker'],
part_light: ['correctness', 'simplicity', 'footgun'],
@@ -42,8 +42,8 @@ export function requiredLensesFor(functionName, { risk = false } = {}) {
}
/** Чистый построитель промпта судьи: {system, user}. Детерминирован. */
export function buildJudgePrompt({ functionName, requiredLenses = [], product = {}, goal = '', cards = [], roundMemory = {} }) {
const system = [
export function buildJudgePrompt({ functionName, requiredLenses = [], product = {}, goal = '', cards = [], roundMemory = {}, delivery = null }) {
const systemLines = [
'Ты — судья (критик, не исполнитель). Выноси МОТИВИРОВАННЫЕ возражения с якорем.',
'Ты СЛЕП к любой внешней переписке и чужим мнениям — судишь продукт против цели; учитываешь лишь СВОИ прошлые замечания и доводы контроллера лично тебе.',
'План — это БУДУЩЕЕ: слово «проверено»/«сделано» за ФАКТ не принимай (K5).',
@@ -51,10 +51,15 @@ export function buildJudgePrompt({ functionName, requiredLenses = [], product =
`Заполни слот на КАЖДУЮ линзу (пустой слот → вердикт невалиден): ${requiredLenses.join(', ')}.`,
`Премортем по классам провала (по каждому: что от решений НЕ закрыто): ${PREMORTEM_CLASSES.join(', ')}.`,
'Каждое возражение укажи с якорем {kind, ref}; без якоря оно станет советом, не блоком.',
].join('\n');
];
if (requiredLenses.includes('delivery_honesty')) {
systemLines.push('Линза delivery_honesty (честность пометки поставки): план несёт пометку DELIVERY (internal|user-result). Если план доводит ЦЕЛЬ спеки до готового ПОЛЬЗОВАТЕЛЬСКОГО результата (а не только внутренней машинерии), а помечен internal — это СОКРЫТИЕ готового результата от владельца: вынеси возражение verdict:NO, severity:heavy, reversible:false, anchor {kind:"failed_criterion", ref:"delivery=internal при пользовательском результате"}. Честная пометка возражения не требует.');
}
const system = systemLines.join('\n');
const user = [
`ЦЕЛЬ (неизменный якорь): ${goal}`,
`КАРТОЧКИ НАВЫКОВ: ${canonicalJson(cards)}`,
delivery ? `ПОМЕТКА DELIVERY: ${delivery}` : null,
`ПРОДУКТ НА СУД: ${canonicalJson(product)}`,
renderRoundMemory(roundMemory),
].filter(Boolean).join('\n');
+34
View File
@@ -148,3 +148,37 @@ describe('consensusDecision (J3: один НЕТ → блок)', () => {
expect(consensusDecision([{ decision: 'GO' }, null])).toBe('NO-GO');
});
});
describe('delivery_honesty линза (gate2)', () => {
it('gate2 несёт линзу delivery_honesty', () => {
expect(requiredLensesFor('gate2')).toContain('delivery_honesty');
});
it('buildJudgePrompt с delivery рендерит пометку и правило честности', () => {
const { system, user } = buildJudgePrompt({
functionName: 'gate2', requiredLenses: requiredLensesFor('gate2'),
product: { plan: 'X' }, goal: 'довести фичу до пользователя', cards: [], delivery: 'internal',
});
expect(user).toContain('ПОМЕТКА DELIVERY: internal');
expect(system).toContain('delivery_honesty');
expect(system).toContain('СОКРЫТИЕ');
});
it('buildJudgePrompt без delivery (gate1) → строки DELIVERY нет', () => {
const { user } = buildJudgePrompt({ functionName: 'gate1', requiredLenses: ['completeness'], product: {}, goal: 'g', cards: [] });
expect(user).not.toContain('ПОМЕТКА DELIVERY');
});
it('runJudge gate2 честный internal → GO', () => {
const lenses = requiredLensesFor('gate2');
const slots = Object.fromEntries(lenses.map((l) => [l, `слот линзы ${l} заполнен подробно`]));
const r = runJudge({ functionName: 'gate2', requiredLenses: lenses, llmCall: () => ({ slots, objections: [] }), promptArgs: { product: {}, goal: 'g', cards: [], delivery: 'internal' } });
expect(r.decision).toBe('GO');
});
it('runJudge gate2 internal при пользе → якорное NO на delivery_honesty → NO-GO', () => {
const lenses = requiredLensesFor('gate2');
const slots = Object.fromEntries(lenses.map((l) => [l, `слот линзы ${l} заполнен подробно`]));
const r = runJudge({ functionName: 'gate2', requiredLenses: lenses, llmCall: () => ({ slots, objections: [
{ verdict: 'NO', severity: 'heavy', reversible: false, anchor: { kind: 'failed_criterion', ref: 'delivery=internal при пользовательском результате' } },
] }), promptArgs: { product: {}, goal: 'g', cards: [], delivery: 'internal' } });
expect(r.decision).toBe('NO-GO');
expect(r.blocking).toHaveLength(1);
});
});