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:
@@ -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);
|
||||
|
||||
@@ -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): критерий ↔ настоящий зелёный прогон', () => {
|
||||
|
||||
Reference in New Issue
Block a user