Files
portal/docs/superpowers/plans/2026-06-05-m3-audit-fixes.md
T

17 KiB
Raw Blame History

Машина 3 — закрытие находок аудита 2026-06-05 — TDD-план

For agentic workers: REQUIRED SUB-SKILL: superpowers:executing-plans. Steps - [ ]. Код строит контроллер inline. commit — owner-gated. Тест-файлы расширяем Write полного файла.

Goal: Закрыть находки построчного аудита Машины 3 настоящими механическими зубами там, где это возможно, и честно задокументировать остаток там, где код закрыть не может (без призраков).

Что закрываем кодом (настоящие зубы):

  • F1 — гард R4 (skill-contract): rationale у inherent отвергается, если содержит маркер прошлого промаха («забыли», «прошлый раз»…) — инъецируемый список. Превращает «почти косметику» в реальный отказ очевидной подмены.
  • F4 — терминатор круга (round-control): невалидный roundCount (отриц/NaN) при пробеле → консервативно escalate-owner (не вечный soft-return). + точная семантика 0-based в доке.
  • Minor cadence-порог: oversizeSignal дефолт 12→20 (точнее под «десятки»).
  • Minor decision-graph footgun: новый buildValidatedGraph (validate→build, бросает на невалидном) — безопасный вход.

Что НЕ закрываем кодом (честный остаток — закрытие = документирование, не фейк):

  • F2 — независимость owner_asks (анти-внушение по смыслу): роутер-ИИ производит и owner_asks, и сверку; код не видит его рассуждение. Реальный зуб (groundVerityTrace, source резолвится) уже есть; «выведено ли независимо» механически непроверяемо (спека R1). Добавить псевдо-проверку = призрак. Закрываем доком-остатком.
  • F3 — «надо ли было рекомендовать домен-навык»: domainCallDiscipline по-честному сторожит исполнение рекомендации, а не её необходимость (это качество выбора роутера — другой слой). Закрываем доком-остатком.

Tech Stack: Node.js ESM, vitest tools-only.

Источник: аудит этой сессии (построчно, по факту) против spec 2026-06-05 §2/§3/§3А/§4.


Task 1: F1 — гард R4 маркеры промаха (skill-contract.mjs)

Files: Modify tools/skill-contract.mjs, Test tools/skill-contract.test.mjs (Write полного + блок; импорт DEFAULT_MISTAKE_MARKERS)

  • Step 1: Тест (добавить DEFAULT_MISTAKE_MARKERS в импорт + блок)
describe('гард R4 усилен — rationale не должен быть исповедью прошлого промаха', () => {
  it('DEFAULT_MISTAKE_MARKERS — непустой список', () => {
    expect(Array.isArray(DEFAULT_MISTAKE_MARKERS)).toBe(true);
    expect(DEFAULT_MISTAKE_MARKERS.length).toBeGreaterThan(0);
  });
  it('rationale с маркером промаха («в прошлый раз забыли») → error', () => {
    const r = validateContract({ ...OWN, inherent: [{ need: 'мобильный', rationale: 'в прошлый раз забыли' }] });
    expect(r.ok).toBe(false); expect(r.errors.some((e) => /промах|R4/i.test(e))).toBe(true);
  });
  it('честная природа-формулировка → ok', () => {
    expect(validateContract({ ...OWN, inherent: [{ need: 'мобильный', rationale: 'фронт по природе делает адаптив' }] }).ok).toBe(true);
  });
  it('маркеры инъецируемы (портативно)', () => {
    const r = validateContract({ ...OWN, inherent: [{ need: 'x', rationale: 'спецслово тут' }] }, { mistakeMarkers: ['спецслово'] });
    expect(r.ok).toBe(false);
  });
});
  • Step 2: RED... skill-contract → FAIL (DEFAULT_MISTAKE_MARKERS undefined + маркер-ассерты).

  • Step 3: Реализацияskill-contract.mjs:

(а) После PREVIEW_FORMS добавить экспорт:

// Маркеры «прошлого промаха» (гард R4): rationale про природу навыка, НЕ про то,
// что мы прошлый раз прошляпили. Generic (не project-specific), инъецируемо.
export const DEFAULT_MISTAKE_MARKERS = Object.freeze([
  'забыл', 'забыли', 'прошлый раз', 'в прошлый раз', 'пропустил', 'пропустили',
  'промах', 'прошляпил', 'упустил', 'не учли', 'в прошлом',
]);

(б) Сигнатура validateContract(c)validateContract(c, { mistakeMarkers = DEFAULT_MISTAKE_MARKERS } = {}).

(в) В блоке валидации inherent, в ветке где rationale непустой — добавить проверку маркера. Заменить ветку проверки rationale:

        else if (typeof it.rationale !== 'string' || !it.rationale.trim())
          errors.push(`inherent[${i}].rationale: non-empty (зачем по природе) required — гард R4`);
        else {
          const low = it.rationale.toLowerCase();
          const hit = (mistakeMarkers || []).find((m) => typeof m === 'string' && m && low.includes(m.toLowerCase()));
          if (hit) errors.push(`inherent[${i}].rationale: похоже на прошлый промах (маркер «${hit}»), нужна «природа навыка» — гард R4`);
        }
  • Step 4: GREEN... skill-contract → PASS.
  • Step 5: Commit (owner-gated)
git -C ".claude/worktrees/brainrepo" add tools/skill-contract.mjs tools/skill-contract.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): гард R4 — отвергать rationale-маркеры прошлого промаха (аудит F1)"

Task 2: F4 — консервативный терминатор круга (round-control.mjs)

Files: Modify tools/round-control.mjs, Test tools/round-control.test.mjs (Write полного + блок)

  • Step 1: Тест (добавить блок)
describe('decideRoundOutcome — консервативность при битом счётчике (аудит F4)', () => {
  it('невалидный roundCount (отриц) при пробеле → escalate-owner (не вечный loop)', () => {
    expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: -1 }).outcome).toBe('escalate-owner');
  });
  it('невалидный roundCount (NaN) при пробеле → escalate-owner', () => {
    expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: NaN }).outcome).toBe('escalate-owner');
  });
  it('битый roundCount, но круг сошёлся → всё равно proceed', () => {
    expect(decideRoundOutcome({ codeOk: true, routerFaithful: true, roundCount: NaN }).outcome).toBe('proceed');
  });
});
  • Step 2: RED — текущий код вернёт soft-return на roundCount:-1 (т.к. -1 < 3) → тест падает.

  • Step 3: Реализация — заменить тело decideRoundOutcome:

export function decideRoundOutcome({ codeOk, routerFaithful, roundCount = 0, maxRounds = 3 } = {}) {
  const gap = !codeOk || !routerFaithful;
  if (!gap) return { outcome: 'proceed', reason: 'круг сошёлся: код + смысл' };
  // roundCount — число завершённых проходов (0-based); escalate при >= maxRounds.
  // Битый/неподанный счётчик → консервативно эскалируем (не зацикливаемся в soft-return).
  const rc = (Number.isInteger(roundCount) && roundCount >= 0) ? roundCount : maxRounds;
  if (rc < maxRounds) return { outcome: 'soft-return', reason: `пробел (код:${codeOk} смысл:${routerFaithful}); проход ${rc}/${maxRounds} — мягкий возврат` };
  return { outcome: 'escalate-owner', reason: `пробел не закрыт за ${maxRounds} прохода — эскалация владельцу (C-12)` };
}

Плюс уточнить docstring: «roundCount — счётчик завершённых проходов (0-based), ОБЯЗАН приходить из неподделываемого источника (журнал М1) при live-wiring; модуль сам не инкрементит — без внешнего инкремента терминатор не сработает».

  • Step 4: GREEN — PASS (старые 12 + новые 3).
  • Step 5: Commit (owner-gated)
git -C ".claude/worktrees/brainrepo" add tools/round-control.mjs tools/round-control.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): round-control — битый roundCount → консервативный escalate (аудит F4)"

Task 3: Minor — oversizeSignal дефолт 12→20 (conversation-cadence.mjs)

Files: Modify tools/conversation-cadence.mjs, Test tools/conversation-cadence.test.mjs (Write полного — подправить порог в существующем тесте)

  • Step 1: Тест — в существующем блоке oversizeSignal заменить «десятки вопросов» на 22 (под новый дефолт 20):
  it('десятки вопросов (≥20) → oversize (дроби на волны)', () => {
    const r = oversizeSignal({ questionCount: 22, themeCount: 1 });
    expect(r.oversize).toBe(true);
    expect(r.reasons.some((x) => /волн/i.test(x))).toBe(true);
  });
  it('дюжина вопросов (12) при дефолте 20 → НЕ oversize', () => {
    expect(oversizeSignal({ questionCount: 12, themeCount: 1 }).oversize).toBe(false);
  });

(остальные тесты блока — без изменений; «пороги — ручки» тест с questionThreshold: 4 остаётся валиден).

  • Step 2: REDquestionCount: 12 → НЕ oversize падает при текущем дефолте 12.

  • Step 3: Реализация — в oversizeSignal дефолт questionThreshold = 1220:

export function oversizeSignal({ questionCount = 0, themeCount = 0 } = {}, { questionThreshold = 20, themeThreshold = 3 } = {}) {
  • Step 4: GREEN — PASS.
  • Step 5: Commit (owner-gated)
git -C ".claude/worktrees/brainrepo" add tools/conversation-cadence.mjs tools/conversation-cadence.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): oversizeSignal дефолт 12→20 (точнее под «десятки», аудит minor)"

Task 4: Minor — безопасный buildValidatedGraph (decision-graph.mjs)

Files: Modify tools/decision-graph.mjs, Test tools/decision-graph.test.mjs (Write полного + блок)

  • Step 1: Тест (импорт +buildValidatedGraph, блок)
describe('buildValidatedGraph (validate→build; бросает на невалидном — аудит footgun)', () => {
  it('валидные секции → тот же граф, что buildDecisionGraph', () => {
    expect(buildValidatedGraph(SECTIONS)).toEqual(buildDecisionGraph(SECTIONS));
  });
  it('висящая зависимость → бросает (не строит молча рёбра на несуществующий узел)', () => {
    expect(() => buildValidatedGraph([{ id: 'a', dependsOn: ['нет'] }])).toThrow(/несуществ|невалид/i);
  });
  it('дубль id → бросает', () => {
    expect(() => buildValidatedGraph([{ id: 'a', dependsOn: [] }, { id: 'a', dependsOn: [] }])).toThrow();
  });
});
  • Step 2: REDbuildValidatedGraph is not a function.

  • Step 3: Реализация — добавить в decision-graph.mjs:

/**
 * Безопасный вход: сперва validateSections, при невалидном — бросает (не строит
 * молча рёбра на несуществующие узлы — аудит-footgun). Иначе — buildDecisionGraph.
 */
export function buildValidatedGraph(sections) {
  const v = validateSections(sections);
  if (!v.ok) throw new Error(`decision-graph невалиден: ${v.errors.join('; ')}`);
  return buildDecisionGraph(sections);
}
  • Step 4: GREEN — PASS.
  • Step 5: Commit (owner-gated)
git -C ".claude/worktrees/brainrepo" add tools/decision-graph.mjs tools/decision-graph.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): decision-graph buildValidatedGraph — безопасный вход (аудит footgun)"

Task 5: Честные доки-остатки (F2 / F3 / cadence / domain — без призраков)

Files: Modify docstrings: tools/router-verity.mjs, tools/domain-skill-discipline.mjs, tools/conversation-cadence.mjs. Без логики → без тестов.

  • Step 1: В router-verity.mjs шапку дополнить честным остатком F2:

«ОСТАТОК (R1, непроверяемо кодом): что owner_asks выведен из сырых слов НЕЗАВИСИМО, а не подогнан под план, механически не проверяется — роутер-ИИ производит и вывод, и сверку. Реальный механический зуб — groundVerityTrace (source обязан резолвиться). Содержательная независимость — на судье/владельце.»

  • Step 2: В domain-skill-discipline.mjs шапку дополнить честным scope F3:

«SCOPE: сторожит ИСПОЛНЕНИЕ рекомендации (recommended→called по журналу), НЕ её НЕОБХОДИМОСТЬ (надо ли было рекомендовать домен-навык — это качество выбора роутера, другой слой; здесь не ловится).»

  • Step 3: В conversation-cadence.mjs шапку дополнить:

«ЭТО ДИСПЕТЧЕР-ДАННЫЕ: checksForEvent возвращает id-проверок; ИСПОЛНЕНИЕ (вызов router-emerge/runVerityCheck/coverage-machine) — на вызывающем при live-wiring. Сам модуль ничего не запускает.»

  • Step 4: Commit (owner-gated)
git -C ".claude/worktrees/brainrepo" add tools/router-verity.mjs tools/domain-skill-discipline.mjs tools/conversation-cadence.mjs
git -C ".claude/worktrees/brainrepo" commit -m "docs(m3): честные остатки аудита F2/F3 + cadence-диспетчер (без призраков)"

Task 6: Регрессия tools-only

  • npx vitest run --root ".claude/worktrees/brainrepo/app" --config "vitest.config.tools.mjs" (dangerouslyDisableSandbox=true) → базлайн 2357 + новые (~12). Ноль RED. Доложить.

Self-Review

Находка Закрытие
F1 R4-гард слаб маркеры промаха в rationale → error (Task 1) — настоящий зуб
F4 терминатор от внешнего счётчика битый счётчик → консерв. escalate + точная дока (Task 2)
F2 анти-внушение по смыслу НЕ кодом (призрак) → честный док-остаток (Task 5)
F3 domain «надо ли» НЕ кодом → честный scope-док (Task 5)
minor cadence-порог 12→20 (Task 3)
minor decision footgun buildValidatedGraph (Task 4)
minor cadence номинальна док-диспетчер (Task 5)

Type-consistency: DEFAULT_MISTAKE_MARKERS/mistakeMarkers/buildValidatedGraph/rc единые; обратная совместимость (validateContract 2-й арг опционален; oversizeSignal порог — ручка). Без призраков: F2/F3 закрыты честным документированием, не фейк-проверкой.