feat: round-memory потолки кругов per-стадия spec/plan SP2c-3a счётчики

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 17:29:33 +03:00
parent dc2c1a3df2
commit fd6046fa92
4 changed files with 27 additions and 7 deletions
+5 -3
View File
@@ -437,10 +437,12 @@ const JUDGE_ESCALATE_AFTER = 3;
* blocked=true → +1; blocked=false (allow) → сброс 0. Возвращает новый счёт.
* fsImpl/dir инъектируемы для тестов. Best-effort — ошибка I/O не ломает судью.
*/
export function bumpJudgeNoGo({ taskId, sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
// Фаза 4: счётчик на СТЭК (спека+план) одной задачи — ключ task-id (sessionId — fallback).
export function bumpJudgeNoGo({ taskId, sessionId, stage, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
// SP2c-3: счётчик на КАЖДУЮ стадию ОТДЕЛЬНО — ключ (task-id + stage), по дизайну §0/§6.
// stage отсутствует → 'all' (backward-compat). sessionId — fallback к task-id.
const safe = String(taskId || sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `judge-nogo-${safe}.json`);
const stageKey = String(stage || 'all').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `judge-nogo-${safe}-${stageKey}.json`);
let count = 0;
try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
const next = blocked ? count + 1 : 0;
+8
View File
@@ -18,6 +18,14 @@ describe('bumpJudgeNoGo per task-id (Фаза 4 — счётчик на стэк
expect(bumpJudgeNoGo({ taskId: 'task:B', blocked: false, fsImpl, dir })).toBe(0);
expect(bumpJudgeNoGo({ taskId: 'task:A', blocked: true, fsImpl, dir })).toBe(3);
});
it('SP2c-3: spec и plan одной задачи → независимые потолки (per-стадия §0/§6)', () => {
const fsImpl = mem(); const dir = '/r';
expect(bumpJudgeNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(1);
expect(bumpJudgeNoGo({ taskId: 'task:A', stage: 'plan', blocked: true, fsImpl, dir })).toBe(1);
expect(bumpJudgeNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(2);
expect(bumpJudgeNoGo({ taskId: 'task:A', stage: 'plan', blocked: false, fsImpl, dir })).toBe(0);
expect(bumpJudgeNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(3);
});
});
describe('bindingHashForJudge (Фаза 3 — какой хеш судья сверяет с mentor-GO)', () => {
+6 -4
View File
@@ -9,11 +9,13 @@ import { runtimeDir } from './enforce-hook-helpers.mjs';
export const MENTOR_ESCALATE_AFTER = 3;
export function bumpMentorNoGo({ taskId, sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
// Фаза 4: счётчик на СТЭК (спека+план) одной задачи — ключ task-id (sessionId — fallback
// backward-compat). Две правки в одной сессии → независимые счётчики.
export function bumpMentorNoGo({ taskId, sessionId, stage, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
// SP2c-3: счётчик на КАЖДУЮ стадию ОТДЕЛЬНО — ключ (task-id + stage), по дизайну §0/§6
// (спека и план — два прогона цикла, у каждого свой потолок ≤3). stage отсутствует → 'all'
// (backward-compat). sessionId — fallback к task-id.
const safe = String(taskId || sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `mentor-nogo-${safe}.json`);
const stageKey = String(stage || 'all').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `mentor-nogo-${safe}-${stageKey}.json`);
let count = 0;
try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
const next = blocked ? count + 1 : 0;
+8
View File
@@ -25,6 +25,14 @@ describe('bumpMentorNoGo', () => {
expect(bumpMentorNoGo({ taskId: 'task:B', blocked: false, fsImpl, dir })).toBe(0); // сброс только B
expect(bumpMentorNoGo({ taskId: 'task:A', blocked: true, fsImpl, dir })).toBe(3); // A не тронут
});
it('SP2c-3: spec и plan одной задачи → независимые потолки (per-стадия §0/§6)', () => {
const fsImpl = mem(); const dir = '/r';
expect(bumpMentorNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(1);
expect(bumpMentorNoGo({ taskId: 'task:A', stage: 'plan', blocked: true, fsImpl, dir })).toBe(1); // план независим от спеки
expect(bumpMentorNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(2);
expect(bumpMentorNoGo({ taskId: 'task:A', stage: 'plan', blocked: false, fsImpl, dir })).toBe(0); // сброс только plan
expect(bumpMentorNoGo({ taskId: 'task:A', stage: 'spec', blocked: true, fsImpl, dir })).toBe(3); // spec не тронут
});
it('порог эскалации = 3', () => { expect(MENTOR_ESCALATE_AFTER).toBe(3); });
it('небезопасный sessionId санитизируется (не падает)', () => {
const fsImpl = mem();