fix(m2): указатель шага привязан к plan_id — перепечать сбрасывает указатель (R-27/J2)
This commit is contained in:
@@ -53,6 +53,18 @@ export function actionOf(toolUse) {
|
||||
return { op, object };
|
||||
}
|
||||
|
||||
/**
|
||||
* R-27: указатель шага привязан к plan_id. Чужой план / легаси (голое число) /
|
||||
* нет plan_id / нет текущего плана → 0 — перепечать плана НЕ отматывает старый
|
||||
* указатель на новый план (иначе шаги нового плана мис-индексируются).
|
||||
*/
|
||||
export function resolveStepPtr(stored, currentPlanId) {
|
||||
if (!currentPlanId || !stored || typeof stored !== 'object') return 0;
|
||||
if (stored.plan_id !== currentPlanId) return 0;
|
||||
const n = Number(stored.ptr);
|
||||
return Number.isInteger(n) && n >= 0 ? n : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Решение верховного хука. allow только если: семя / зелёный проход ИЛИ действие
|
||||
* совпадает с текущим шагом валидного замороженного плана, ссылка шага резолвится
|
||||
@@ -126,11 +138,12 @@ async function main() {
|
||||
const frozenPlan = loadFrozenPlan({ sessionId: sess, runtimeDir });
|
||||
const frozenArtifact = loadFrozenArtifact({ sessionId: sess, runtimeDir });
|
||||
const stepPath = `${runtimeDir}/plan-step-${sess}`;
|
||||
let stepPtr = 0; try { stepPtr = parseInt(fs.readFileSync(stepPath, 'utf8'), 10) || 0; } catch {}
|
||||
let stored = null; try { stored = JSON.parse(fs.readFileSync(stepPath, 'utf8')); } catch {}
|
||||
const stepPtr = resolveStepPtr(stored, frozenPlan?.plan_id); // R-27: привязка к плану
|
||||
const r = runGate({
|
||||
event, frozenPlan, frozenArtifact, stepPtr, key,
|
||||
journal: (entry) => journalAppend({ payload: entry, key, sessionId: sess, runtimeDir }),
|
||||
saveStep: (n) => fs.writeFileSync(stepPath, String(n)),
|
||||
saveStep: (n) => fs.writeFileSync(stepPath, JSON.stringify({ plan_id: frozenPlan?.plan_id ?? null, ptr: n })),
|
||||
});
|
||||
exitDecision({ block: r.block, message: r.block ? `[supreme-gate] ${r.message}` : undefined });
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,24 @@ import { decide, actionOf } from './enforce-supreme-gate.mjs';
|
||||
import { runGate } from './enforce-supreme-gate.mjs';
|
||||
import { decideMode } from './enforce-supreme-gate.mjs';
|
||||
import { isObserveOnly } from './enforce-supreme-gate.mjs';
|
||||
import { resolveStepPtr } from './enforce-supreme-gate.mjs';
|
||||
|
||||
describe('resolveStepPtr — указатель шага привязан к plan_id (R-27)', () => {
|
||||
it('тот же план → возвращает сохранённый ptr', () => {
|
||||
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, 'P1')).toBe(3);
|
||||
});
|
||||
it('другой план → сброс в 0 (перепечать не отматывает чужой указатель)', () => {
|
||||
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, 'P2')).toBe(0);
|
||||
});
|
||||
it('легаси (голое число) / null / без plan_id → 0', () => {
|
||||
expect(resolveStepPtr(5, 'P1')).toBe(0);
|
||||
expect(resolveStepPtr(null, 'P1')).toBe(0);
|
||||
expect(resolveStepPtr({ ptr: 9 }, 'P1')).toBe(0);
|
||||
});
|
||||
it('нет текущего плана → 0', () => {
|
||||
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, undefined)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSeed (D12/D13 bootstrap exemption)', () => {
|
||||
it('EnterPlanMode and AskUserQuestion are seeds', () => {
|
||||
|
||||
Reference in New Issue
Block a user