You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
session:sums checksums: rounds=turns is always true, hookParity is one-sided and off by one #102
⚠️Выводы не проверены человеком. Ишью заведено по аудиту, который сделал Claude Code. Перед исправлением достаточно один раз выполнить блок «Как проверить» и сравнить вывод с ожидаемым.
Что не так
pnpm session:sums (из #69) печатает checksums, которые должны доказывать, что числа сходятся. Две проверки не проверяют того, что обещают:
roundsSumEqualsTurnsSum всегда true. Обе стороны — одна и та же сумма по roundsByTurn. Если испортить раунды, она остаётся true; порчу видит только turnsSumEqualsSession. Ревью fix(turn): compaction is not a turn — one numbering everywhere #69 отмечало это дважды — не исправлено.
hookParity односторонняя и сдвинута на 1. Сверяются только ходы ledger, а ходы хука без пары в ledger не проверяются. К тому же buildLedger открывает неявный ход для раундов до первого user-сообщения, а hookSpentByTurn даёт им номер 0. Для сессии, которая начинается с релея или сводки компакта, получается ложное match: false со сдвигом на один ход, хотя числа одни и те же.
Как проверить (один раз, ~1 мин)
Из корня репозитория:
cat > /tmp/repro-sums.mts <<'EOF'import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';import { tmpdir } from 'node:os';import { join } from 'node:path';const R = process.cwd(); // запускать из корня репозиторияconst load = (p: string) => import(`${R}/${p}`);let t = Date.parse('2026-10-01T07:00:00Z'), n = 0;const at = (ms = 20_000) => new Date((t += ms)).toISOString();const base = (x = {}) => ({ uuid: `u${++n}`, parentUuid: null, sessionId: 's', cwd: '/p', isSidechain: false, ...x });const user = (content: unknown, x = {}, gap?: number) => ({ ...base(x), type: 'user', timestamp: at(gap), message: { role: 'user', content } });const ai = (block: object, ctx: number, out: number, ids: { rid?: string; mid: string }, x = {}, gap?: number) => ({ ...base(x), type: 'assistant', timestamp: at(gap), ...(ids.rid ? { requestId: ids.rid } : {}), message: { id: ids.mid, role: 'assistant', model: 'claude-x', content: [block], usage: { input_tokens: 0, cache_read_input_tokens: ctx, cache_creation_input_tokens: 0, output_tokens: out } } });const text = (s: string) => ({ type: 'text', text: s });const write = (lines: object[]) => { const f = join(mkdtempSync(join(tmpdir(), 'repro-')), 's.jsonl'); writeFileSync(f, lines.map((l) => JSON.stringify(l)).join('\n') + '\n'); return f; };const jsonl = await load('src/main/utils/jsonl.ts');const cli = await load('src/cli/analyzeSession.ts');const sums = await load('src/cli/sessionSums.ts');const run = async (lines: object[], corrupt = false) => { const f = write(lines); const msgs = await jsonl.parseJsonlFile(f); const ledger = cli.buildLedger(msgs); if (corrupt) for (const r of ledger.rounds) r.inputTokens += 777; // испортить раунды ПОСЛЕ построения итогов return sums.buildSums(ledger, cli.computeFindings(msgs, ledger), sums.hookSpentByTurn(readFileSync(f, 'utf8').split('\n'))).checksums;};const plain = [user('q1'), ai(text('a'), 100, 1, { rid: 'q1', mid: 'm1' }), user('q2'), ai(text('b'), 400, 1, { rid: 'q2', mid: 'm2' })];// 1) roundsSumEqualsTurnsSum не может упасть: обе стороны — одна и та же суммаconst c = await run(plain, true);console.log(`испорченные раунды → roundsSumEqualsTurnsSum = ${c.roundsSumEqualsTurnsSum}, turnsSumEqualsSession = ${c.turnsSumEqualsSession} (порчу видит только вторая: первая сравнивает сумму саму с собой)`);// 2) hookParity: сессия начинается с релея (или со сводки компакта) — номера ходов сдвинуты на 1const relayFirst = [user('<teammate-message teammate_id="lead">do X</teammate-message>', { isMeta: false }), ai(text('a'), 100, 1, { rid: 'r1', mid: 'n1' }), ai(text('b'), 200, 1, { rid: 'r2', mid: 'n2' }), user('real question'), ai(text('c'), 400, 1, { rid: 'r3', mid: 'n3' })];const hp = (await run(relayFirst)).hookParity;console.log('сессия с релеем в начале → hookParity =', JSON.stringify(hp), '(ожидаемо match: true — числа одни и те же)');console.log(!hp.match && c.roundsSumEqualsTurnsSum ? 'ВОСПРОИЗВЕДЕНО: контрольные суммы session:sums не проверяют то, что обещают' : 'OK');EOF
pnpm exec tsx /tmp/repro-sums.mts
Правильно: после порчи раундов roundsSumEqualsTurnsSum = false; для сессии с релеем в начале hookParity.match = true. Сейчас:roundsSumEqualsTurnsSum = true и hookParity = {"match":false,"mismatches":[{"turn":1,"ledger":300,"hook":400},{"turn":2,"ledger":400,"hook":0}]}, строка ВОСПРОИЗВЕДЕНО.
Part of #94.
Что не так
pnpm session:sums(из #69) печатаетchecksums, которые должны доказывать, что числа сходятся. Две проверки не проверяют того, что обещают:roundsSumEqualsTurnsSumвсегдаtrue. Обе стороны — одна и та же сумма поroundsByTurn. Если испортить раунды, она остаётсяtrue; порчу видит толькоturnsSumEqualsSession. Ревью fix(turn): compaction is not a turn — one numbering everywhere #69 отмечало это дважды — не исправлено.hookParityодносторонняя и сдвинута на 1. Сверяются только ходы ledger, а ходы хука без пары в ledger не проверяются. К тому жеbuildLedgerоткрывает неявный ход для раундов до первого user-сообщения, аhookSpentByTurnдаёт им номер 0. Для сессии, которая начинается с релея или сводки компакта, получается ложноеmatch: falseсо сдвигом на один ход, хотя числа одни и те же.Как проверить (один раз, ~1 мин)
Из корня репозитория:
Правильно: после порчи раундов
roundsSumEqualsTurnsSum = false; для сессии с релеем в началеhookParity.match = true. Сейчас:roundsSumEqualsTurnsSum = trueиhookParity = {"match":false,"mismatches":[{"turn":1,"ledger":300,"hook":400},{"turn":2,"ledger":400,"hook":0}]}, строкаВОСПРОИЗВЕДЕНО.Где в коде (гипотеза)
src/cli/sessionSums.ts:104—roundsEqualTurnsпересчитывает ту же сумму, из которой построенturns.src/cli/sessionSums.ts:111—hookMismatchesперебирает только ходы ledger.buildLedger(currentTurn ?? newTurn) против хода 0 вhookSpentByTurn(isTurnNumberLine).Что сделать
roundsSumEqualsTurnsSumили заменить настоящей проверкой — например, сумма раундов, посчитанная из сырых строк, против суммы ходов.hookParity: сверять в обе стороны (объединение номеров ходов) и одинаково нумеровать раунды до первого user-сообщения (общее решение с остальными ишью эпика Epic: one number on every surface — cross-surface parity (audit follow-ups) #94 про нумерацию ходов).Acceptance
hookParity.match = true