feat(m5): механическая значимость шага в k5CriterionCheck (Пакет 5, 5.2, C3/F18)

Значимость шага больше НЕ из контроллер-флагов trivial/significant (их подделка
разжаловала значимый шаг и обнуляла K5). Новый isSignificantStep(step):
- object разрушителен (classifyDestructive.suspicious) → значим, перевешивает op
  (анти-обход «op:Read, object: rm -rf»);
- op НЕ из READONLY_OPS (Read/Grep/Glob/LS/NotebookRead) → значим (мутирующий/
  неизвестный/пустой op → сомнение → значим, fail-CLOSE);
- чистое чтение → не значим, критерий не требуется.
k5CriterionCheck вызывает isSignificantStep вместо trivial/significant-скипов.

+7 тестов. Полная tools-only регрессия 2690 passed + 2 skip (0 регрессий).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-07 12:42:36 +03:00
parent be3966dbad
commit 4e69a3bc0c
2 changed files with 65 additions and 5 deletions
+23 -3
View File
@@ -11,6 +11,7 @@
* - skillTakenByJournal (A2, K2): навык взят по журналу вызовов, не по тексту TodoWrite.
* Тупо-механика: уболтать нельзя. Качество — думающая часть 4-D + владелец.
*/
import { classifyDestructive } from './classify-destructive.mjs';
/** Обещанные навыки реально вызваны (по журналу)? Общий помощник для Гейта-1 и A2. */
function skillsCoveredByJournal(promised = [], invoked = []) {
@@ -36,12 +37,31 @@ export function specToPlanCoverage({ specSections = [], planSteps = [] }) {
const BARE_DONE_RE = /^(проверено|проверен[аоы]|сделано|готово|ок|done|passed|pass|ready)\.?$/i;
const MIN_CRITERION_LEN = 6;
/** Гейт-2 K5: значимый шаг → конкретный проверяемый критерий; «проверено» не факт. */
// Чисто-читающие инструменты: не мутируют состояние → шаг не значим, критерий не нужен.
const READONLY_OPS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
/**
* 5.2 (C3/F18): МЕХАНИЧЕСКАЯ значимость шага — НЕ из контроллер-флагов. Значим, ЕСЛИ:
* - object разрушителен (classifyDestructive.suspicious) — перевешивает любой op (анти-обход
* «op:Read, object: rm -rf»); ИЛИ
* - op НЕ из чисто-читающего набора (мутирующий / неизвестный / пустой op → сомнение → значим).
* Контроллер-флаги trivial/significant ИГНОРИРУЮТСЯ: их подделка больше не разжалует значимый шаг.
*/
export function isSignificantStep(step) {
if (!step || typeof step !== 'object') return true; // сомнение → значим (fail-CLOSE)
const object = String(step.object ?? step.command ?? '');
if (classifyDestructive(object).suspicious) return true; // разрушительное → всегда значимо
const op = String(step.op || '');
if (READONLY_OPS.has(op)) return false; // чистое чтение — критерий не нужен
return true; // мутирующий / неизвестный op → значим
}
/** Гейт-2 K5: значимый шаг → конкретный проверяемый критерий; «проверено» не факт.
* Значимость — механическая (isSignificantStep), флаги контроллера не учитываются (C3/F18). */
export function k5CriterionCheck({ planSteps = [] }) {
const missingCriterion = [];
for (const s of planSteps || []) {
if (s && s.trivial === true) continue; // механический микрошаг — критерий не нужен
if (s && s.significant === false) continue; // явно НЕ значим
if (!isSignificantStep(s)) continue; // механически не значим (чистое чтение)
const c = s && typeof s.criterion === 'string' ? s.criterion.trim() : '';
const concrete = c.length >= MIN_CRITERION_LEN && !BARE_DONE_RE.test(c);
if (!concrete) missingCriterion.push(s.n);
+42 -2
View File
@@ -39,6 +39,11 @@ describe('specToPlanCoverage (Гейт-2 пол): каждый § спеки п
});
});
// 5.2 (C3/F18): значимость шага — МЕХАНИЧЕСКАЯ (из класса действия в judge-gate-floor),
// НЕ из контроллер-флагов trivial/significant. Флаги больше не разжалуют значимый шаг;
// разжаловать может только реально-читающий op. Разрушительный object перевешивает op.
import { isSignificantStep } from './judge-gate-floor.mjs';
describe('k5CriterionCheck (Гейт-2, K5): значимый шаг → конкретный критерий; «проверено» не факт', () => {
it('значимый шаг с конкретным критерием → ok', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, criterion: 'тест RlsSmokeTest проходит' }] });
@@ -54,8 +59,8 @@ describe('k5CriterionCheck (Гейт-2, K5): значимый шаг → кон
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([1, 2]);
});
it('механический микрошаг (trivial) → критерий не требуется', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, trivial: true }] });
it('чистое чтение (op:Read) без критерия → не требуется (механически не значим)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, op: 'Read', object: 'tools/x.mjs' }] });
expect(r.ok).toBe(true);
});
it('сомнение в значимости → шаг считается значимым (нельзя «разжаловать» чтобы проскочить)', () => {
@@ -63,6 +68,41 @@ describe('k5CriterionCheck (Гейт-2, K5): значимый шаг → кон
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([5]);
});
it('C3/F18: контроллер-флаг trivial:true НЕ разжалует мутирующий шаг (Write без критерия → missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 7, op: 'Write', object: 'tools/x.mjs', trivial: true }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([7]);
});
it('C3/F18: контроллер-флаг significant:false НЕ разжалует мутирующий шаг (Bash без критерия → missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 8, op: 'Bash', object: 'git commit', significant: false }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([8]);
});
it('обход через op:Read при разрушительном object → всё равно значим (missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 9, op: 'Read', object: 'rm -rf build' }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([9]);
});
});
describe('isSignificantStep (5.2): механическая значимость по классу действия', () => {
it('мутирующие op значимы (Write/Edit/MultiEdit/NotebookEdit/Bash)', () => {
for (const op of ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash']) {
expect(isSignificantStep({ op, object: 'x' })).toBe(true);
}
});
it('чистое чтение не значимо (Read/Grep/Glob/LS/NotebookRead)', () => {
for (const op of ['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']) {
expect(isSignificantStep({ op, object: 'x' })).toBe(false);
}
});
it('разрушительный object перевешивает readonly-op (анти-обход)', () => {
expect(isSignificantStep({ op: 'Read', object: 'git push --force' })).toBe(true);
});
it('неизвестный/пустой op → значим (сомнение → значим, fail-CLOSE)', () => {
expect(isSignificantStep({ n: 1 })).toBe(true);
expect(isSignificantStep(null)).toBe(true);
});
});
describe('criteriaGreenMatched (Гейт-3, $0): критерий ↔ настоящий зелёный прогон', () => {