17 KiB
Машина 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_MARKERSundefined + маркер-ассерты). -
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: RED —
questionCount: 12 → НЕ oversizeпадает при текущем дефолте 12. -
Step 3: Реализация — в
oversizeSignalдефолтquestionThreshold = 12→20:
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: RED —
buildValidatedGraph 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 закрыты честным документированием, не фейк-проверкой.