From 3663705ddead50b07e75bed7c7e1fb0c8ef93d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 9 Jun 2026 07:20:48 +0300 Subject: [PATCH] fix(shadow-replay): exclude runtime writes and scope M6 to bash --- tools/shadow-replay.mjs | 16 +++++++++++++--- tools/shadow-replay.test.mjs | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/tools/shadow-replay.mjs b/tools/shadow-replay.mjs index 76127695..f4f07f9c 100644 --- a/tools/shadow-replay.mjs +++ b/tools/shadow-replay.mjs @@ -134,12 +134,20 @@ export function loadEpisodes(dir = 'docs/observer', fsImpl = fsDefault) { return out; } -/** Приближённые безобидные file-события из эпизодов: каждый touched-файл → Edit. */ +// Запись в runtime/секрет — НЕ benign правка обычного файла (пол блокирует по делу); исключаем +// из приближённых безобидных событий, иначе даёт ложный over-block (отчёт 2026-06-09). +const RUNTIME_OR_SECRET_RE = /\.claude[\\/]runtime[\\/]|(^|[\\/])\.env(\.[\w-]+)?$|\.(pem|key|p12|pfx)$|id_(rsa|dsa|ecdsa|ed25519)/i; + +/** Приближённые безобидные file-события из эпизодов: touched-файл → Edit (кроме runtime/секрет). */ export function episodeBenignEvents(records) { const out = []; for (const r of records || []) { const files = (r && r.task_size && Array.isArray(r.task_size.files)) ? r.task_size.files : []; - for (const fp of files) if (typeof fp === 'string' && fp) out.push({ name: 'Edit', input: { file_path: fp } }); + for (const fp of files) { + if (typeof fp !== 'string' || !fp) continue; + if (RUNTIME_OR_SECRET_RE.test(fp)) continue; + out.push({ name: 'Edit', input: { file_path: fp } }); + } } return out; } @@ -218,9 +226,11 @@ export function renderReport(byMachine, { divergence } = {}) { /** Прогнать все 5 машин + М3-расхождение, вернуть {md, byMachine, divergence}. */ export function runAll({ episodeRecords = [] } = {}) { const corpus = buildCorpus({ episodeRecords }); + // М6 (снимок) — зона только Bash-floor; не-Bash рискованное держит пол М5, не М6 (отчёт 2026-06-09). + const corpusM6 = corpus.filter((e) => e.kind !== 'risky' || (e.toolUse && e.toolUse.name === 'Bash')); const byMachine = { M5: summarize(runMachine('M5', corpus)), - M6: summarize(runMachine('M6', corpus)), + M6: summarize(runMachine('M6', corpusM6)), M2: summarize(runMachine('M2', M2_EVENTS, { frozenPlan: freezeReplayPlan(M2_PLAN_STEPS) })), M4: summarize(runMachine('M4', M4_FIXTURE)), }; diff --git a/tools/shadow-replay.test.mjs b/tools/shadow-replay.test.mjs index 36c9caaa..01c285fa 100644 --- a/tools/shadow-replay.test.mjs +++ b/tools/shadow-replay.test.mjs @@ -181,3 +181,25 @@ describe('runAll', () => { expect(r.divergence.total).toBe(0); }); }); + +describe('harness fixes (отчёт 2026-06-09)', () => { + it('episodeBenignEvents исключает запись в runtime/секрет (не benign)', () => { + const recs = [{ task_size: { files: [ + 'app/A.php', + 'C:/Users/x/.claude/runtime/router-state.json', + 'app/.env', + ] } }]; + expect(episodeBenignEvents(recs)).toEqual([ + { name: 'Edit', input: { file_path: 'app/A.php' } }, + ]); + }); + it('runAll: запись в runtime из эпизода НЕ даёт ложный over-block М5', () => { + const r = runAll({ episodeRecords: [{ task_size: { files: ['C:/Users/x/.claude/runtime/state.json'] } }] }); + expect(r.byMachine.M5.counts['over-block']).toBe(0); + }); + it('runAll: М6 не штрафуется за не-Bash рискованное (miss=0), но ловит Bash-floor (real-catch=2)', () => { + const r = runAll({ episodeRecords: [] }); + expect(r.byMachine.M6.counts.miss).toBe(0); + expect(r.byMachine.M6.counts['real-catch']).toBe(2); + }); +});