docs(router-gate-v4): calibration 5 plan - cosmetic-detector git-approval exemption
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Calibration 5 — cosmetic-detector excludes git-approval AskUser Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:test-driven-development. Steps use checkbox (`- [ ]`) syntax.
|
||||
|
||||
**Goal:** Stop `askuser-cosmetic-detector` from counting/blocking git-operation **approval** AskUsers as "cosmetic A/B" — a scope fix that does NOT lower discipline.
|
||||
|
||||
**Architecture:** The detector's target is *simple A/B questions used instead of structured ideation* (brainstorming/writing-plans). A git-approval AskUser (one whose option label is a literal git command) is the *sanctioned git-approval channel* (`enforce-askuser-answer-parser` writes `approve_git_operation` from it) — it is never a substitute for ideation. We add `isGitApprovalQuestion()` and short-circuit `decide()` to `allow` with `isSimpleAB:false` for such questions, so they are neither counted toward the session limit nor hard-blocked. Everything else is unchanged.
|
||||
|
||||
**Tech Stack:** Node ESM `.mjs`, vitest.
|
||||
|
||||
**Why NO discipline hole (adversarial check):**
|
||||
|
||||
- The real target (design-clarification A/B like "Вариант A"/"Вариант B") has NON-git labels → still classified simple → still counted → still hard-blocked at >2. Unchanged.
|
||||
- A git-approval question is identified ONLY by an option label matching a git-command verb. To "disguise" a cosmetic clarification as exempt, the controller would have to put a literal `git …` command as an option label — but then the chosen answer IS a git command, which `enforce-askuser-answer-parser` turns into a real `approve_git_operation` record; it cannot function as a cosmetic ideation-dodge. So there is no usable bypass.
|
||||
- Exemption is narrow and structural (label is a git command), mirroring calibrations 1 (Skill) / 3 (test-runner) / 4 (user-prompt fallback): scope fix, not a discipline drop.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: isGitApprovalQuestion + decide() exemption
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/askuser-cosmetic-detector.mjs`
|
||||
- Test: `tools/askuser-cosmetic-detector.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```javascript
|
||||
import { isGitApprovalQuestion } from './askuser-cosmetic-detector.mjs';
|
||||
|
||||
describe('isGitApprovalQuestion (calibration 5)', () => {
|
||||
it('true when an option label is a git command', () => {
|
||||
expect(isGitApprovalQuestion([{ options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] }])).toBe(true);
|
||||
expect(isGitApprovalQuestion([{ options: [{ label: 'git commit -F x -- a b' }, { label: 'Отмена' }] }])).toBe(true);
|
||||
});
|
||||
it('false for a non-git A/B', () => {
|
||||
expect(isGitApprovalQuestion([{ options: [{ label: 'Вариант А' }, { label: 'Вариант Б' }] }])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// decide(): git-approval question is exempt — allow, not simple, not counted, never blocked even past the session limit.
|
||||
describe('decide — git-approval exemption (calibration 5)', () => {
|
||||
it('allows a git-approval question and does NOT count it even when session is already over the limit', () => {
|
||||
const r = decide({
|
||||
questions: [{ options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] }],
|
||||
simpleCountSession: 5, 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
|
||||
});
|
||||
|
||||
it('REGRESSION: a non-git simple A/B past the limit STILL hard-blocks (discipline intact)', () => {
|
||||
const r = decide({
|
||||
questions: [{ options: [{ label: 'A' }, { label: 'B' }] }],
|
||||
simpleCountSession: 5, brainstormingInvoked: false,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.action).toBe('hard_block');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run RED** — `npx vitest run --root app --config vitest.config.tools.mjs askuser-cosmetic-detector` → fail (isGitApprovalQuestion missing; git-approval not exempt).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
Add near `isSimpleAB`:
|
||||
|
||||
```javascript
|
||||
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)));
|
||||
}
|
||||
```
|
||||
|
||||
In `decide()`, replace `const simple = isSimpleAB(questions);` with:
|
||||
|
||||
```javascript
|
||||
// Calibration 5: git-operation approval prompts are the sanctioned approval
|
||||
// channel, never cosmetic ideation — exempt from the simple-AB count/block.
|
||||
if (isGitApprovalQuestion(questions)) {
|
||||
return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn };
|
||||
}
|
||||
const simple = isSimpleAB(questions);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run GREEN** — same command → pass.
|
||||
|
||||
- [ ] **Step 5: Full regression** — `npx vitest run --root app --config vitest.config.tools.mjs` → all green.
|
||||
|
||||
- [ ] **Step 6: Commit** (with git-approval).
|
||||
Reference in New Issue
Block a user