fix(router-gate-v4): calibration 5 - cosmetic-detector exempts git-approval AskUser (scope fix, regression-tested)

This commit is contained in:
Дмитрий
2026-05-31 11:19:14 +03:00
parent 2c83433386
commit cd2da132a3
2 changed files with 65 additions and 0 deletions
+23
View File
@@ -34,6 +34,22 @@ export function isSimpleAB(questions) {
);
}
// Calibration 5 (2026-05-31) — git-operation APPROVAL prompts are the sanctioned
// git-approval channel (enforce-askuser-answer-parser turns the chosen answer
// into an approve_git_operation record), never a substitute for structured
// ideation. They must NOT be treated as cosmetic A/B. Identified structurally:
// an option label is a literal git command. (SCOPE fix, not a discipline drop —
// see decide(): design A/B questions with non-git labels are unaffected.)
const GIT_CMD_RE = /\bgit\s+(?:commit|push|add|pull|merge|rebase|reset|checkout|switch|branch|stash|cherry-pick|revert|clean|restore|fetch|tag)\b/i;
/** True if this AskUser is a git-operation approval prompt (an option label is a git command). */
export function isGitApprovalQuestion(questions) {
if (!Array.isArray(questions)) return false;
return questions.some((q) =>
q && Array.isArray(q.options) &&
q.options.some((o) => o && typeof o.label === 'string' && GIT_CMD_RE.test(o.label)));
}
/**
* Pure cosmetic-AskUser decision (v4.1 §4.5).
* Caller passes PRIOR counts; decide computes prospective new counts.
@@ -42,6 +58,13 @@ export function isSimpleAB(questions) {
* @returns {{action:'allow'|'soft_flag'|'hard_block', block:boolean, reason:string|null, isSimpleAB:boolean, newSessionCount:number, newTurnCount:number}}
*/
export function decide({ questions, simpleCountSession = 0, simpleCountTurn = 0, skillMatchedThisTurn = false, brainstormingInvoked = false }) {
// Calibration 5: git-operation approval prompts are exempt — the sanctioned
// git-approval channel, never cosmetic ideation. Allow, do not count, never
// block. (Cannot be abused to dodge ideation discipline: a git-command label
// makes the answer a real approve_git_operation, not a cosmetic clarification.)
if (isGitApprovalQuestion(questions)) {
return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn };
}
const simple = isSimpleAB(questions);
const newSessionCount = simpleCountSession + (simple ? 1 : 0);
const newTurnCount = simpleCountTurn + (simple ? 1 : 0);
+42
View File
@@ -92,3 +92,45 @@ describe('askuser-cosmetic-detector / transcript helpers', () => {
expect(countSimpleSession(flags)).toBe(2);
});
});
import { isGitApprovalQuestion } from './askuser-cosmetic-detector.mjs';
// Calibration 5 (2026-05-31, SCOPE fix, NOT a discipline drop): a git-operation
// APPROVAL AskUser (an option label is a literal git command) is the sanctioned
// git-approval channel — enforce-askuser-answer-parser turns the chosen answer
// into an approve_git_operation record. It is never a substitute for structured
// ideation, so it must not be counted/blocked as "cosmetic A/B". Design A/B
// questions (non-git labels) are unchanged — still counted, still hard-blocked.
describe('isGitApprovalQuestion (calibration 5)', () => {
it('true when an option label is a git command (push)', () => {
expect(isGitApprovalQuestion([{ options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] }])).toBe(true);
});
it('true when an option label is a git command (commit with pathspec)', () => {
expect(isGitApprovalQuestion([{ options: [{ label: 'git commit -F x.txt -- a.mjs b.mjs' }, { label: 'Отмена' }] }])).toBe(true);
});
it('false for a non-git A/B', () => {
expect(isGitApprovalQuestion([{ options: [{ label: 'Вариант А' }, { label: 'Вариант Б' }] }])).toBe(false);
});
it('false for empty/invalid input', () => {
expect(isGitApprovalQuestion(null)).toBe(false);
expect(isGitApprovalQuestion([])).toBe(false);
});
});
describe('decide — git-approval exemption (calibration 5)', () => {
const gitQ = { question: 'Подтверди?', options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] };
it('allows a git-approval question and does NOT count it even past the session limit', () => {
const r = decide({ questions: [gitQ], simpleCountSession: 5, simpleCountTurn: 0, skillMatchedThisTurn: false, brainstormingInvoked: false });
expect(r.block).toBe(false);
expect(r.action).toBe('allow');
expect(r.isSimpleAB).toBe(false);
expect(r.newSessionCount).toBe(5); // unchanged — not counted toward the cosmetic limit
});
it('REGRESSION: a non-git simple A/B past the limit STILL hard-blocks (discipline intact)', () => {
const r = decide({ questions: [simpleQ], simpleCountSession: 5, simpleCountTurn: 0, skillMatchedThisTurn: false, brainstormingInvoked: false });
expect(r.action).toBe('hard_block');
expect(r.block).toBe(true);
});
});