refactor(router-gate-v4): stub override helpers (universal vocab removed per spec §4.2)

findOverride/findOverrideAttempt/loadOverrideVocab become permanent stubs returning null/null/empty.
Non-deleted hooks (verify-before-push, tdd-gate, memory-coverage, branch-switch) still import these
symbols and need them to compile; runtime always reports 'no override'.

Adapted 15 existing tests in enforce-hook-helpers.test.mjs and 7 in enforce-semgrep-security.test.mjs
that asserted old vocab behaviour; all now assert stub behaviour (null/empty).
1824/1824 vitest tools GREEN.

Stream G of router-gate v4 deployment.
This commit is contained in:
Дмитрий
2026-05-30 05:55:46 +03:00
parent 97d7deff99
commit d3e192abf1
4 changed files with 103 additions and 167 deletions
+19 -19
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-29T15:20:30.351Z
Last updated: 2026-05-30T02:43:07.509Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,14 +8,14 @@ Last updated: 2026-05-29T15:20:30.351Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ⚠️ | 651 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
| C5 Observer-coverage | ⚠️ | 632 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Метрики (информационные, не алерты)
- Observer evidence: 651 episodes this month, 0 observer_error markers, 144 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 512
- Last /brain-retro: 2 day(s) ago
- Observer evidence: 632 episodes this month, 0 observer_error markers, 129 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 493
- Last /brain-retro: 3 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
## Метрики дисциплины
@@ -24,16 +24,16 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| analysis | 29 | 31.0% | 13.8% |
| bugfix | 20 | 25.0% | 25.0% |
| planning | 18 | 16.7% | 16.7% |
| feature | 17 | 11.8% | 0.0% |
| analysis | 26 | 30.8% | 15.4% |
| bugfix | 19 | 26.3% | 26.3% |
| planning | 16 | 18.8% | 18.8% |
| feature | 15 | 13.3% | 0.0% |
| cleanup | 6 | 0.0% | 0.0% |
| refactor | 1 | 0.0% | 0.0% |
Router step distribution: 1: 275, 2: 238, 3: 70, 5: 61
Router step distribution: 1: 274, 2: 227, 3: 63, 5: 61
Boundaries applied (ADR / границы): 84 of 644 эпизодов (13.0%).
Boundaries applied (ADR / границы): 72 of 625 эпизодов (11.5%).
## Активные многоэтапные проекты
@@ -51,10 +51,10 @@ Boundaries applied (ADR / границы): 84 of 644 эпизодов (13.0%).
| Компонент | Токены (in/out) | USD |
|---|---|---|
| Classifier (Sonnet 4.6) | 3629/44428 | $0.68 |
| Classifier (Sonnet 4.6) | 2748/38452 | $0.59 |
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
| **Итого** | | **$0.68** |
| **Итого** | | **$0.59** |
## Аномалии классификатора
@@ -67,7 +67,7 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 651.
0 эпизодов проверено из 632.
## Reviewer findings
@@ -109,11 +109,11 @@ Episodes since last run: 542 / threshold: 10
| Фраза | За всё время | За сегодня |
|---|---|---|
| `recovery` | 1451 | 554 ⚠️ |
| `без скилов` | 407 | 229 ⚠️ |
| `ремонт инфраструктуры` | 331 | 146 ⚠️ |
| `срочно` | 225 | 132 ⚠️ |
| `memory dump` | 46 | 29 ⚠️ |
| `recovery` | 2295 | 16 ⚠️ |
| `без скилов` | 507 | 40 ⚠️ |
| `ремонт инфраструктуры` | 331 | 0 |
| `срочно` | 225 | 0 |
| `memory dump` | 46 | 0 |
| `direct ok` | 6 | 0 |
| `быстрый коммит` | 3 | 0 |
+8 -48
View File
@@ -1,4 +1,4 @@
/**
/**
* Shared helpers for the 10-rule enforcement hook layer.
*
* Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md
@@ -193,61 +193,21 @@ export function turnToolResults(entries) {
return results;
}
let _vocabCache = null;
export function loadOverrideVocab(path) {
if (_vocabCache) return _vocabCache;
try {
const p = path || join(__dirname, 'enforce-override-vocab.json');
if (!existsSync(p)) return { phrases: [] };
_vocabCache = JSON.parse(readFileSync(p, 'utf-8'));
return _vocabCache;
} catch { return { phrases: [] }; }
// v4 stubs — universal vocab override surface removed per spec §4.2.
// Keep symbols exported so callers in other hooks compile; runtime returns null/empty.
export function loadOverrideVocab(_path) {
return { phrases: [] };
}
export function _resetVocabCache() { _vocabCache = null; }
export function _resetVocabCache() { /* no-op, vocab disabled */ }
export function findOverride(userPrompt, ruleKey, vocab) {
if (!userPrompt || typeof userPrompt !== 'string') return null;
const v = vocab || loadOverrideVocab();
const lo = userPrompt.toLowerCase();
for (const p of v.phrases || []) {
if (!p.phrase || !Array.isArray(p.suppresses)) continue;
if (!lo.includes(p.phrase.toLowerCase())) continue;
if (!p.suppresses.includes(ruleKey)) continue;
if (p.requires_justification) {
// Hole 7 fix: master overrides require a line "<prefix> <non-empty>"
// in the same prompt documenting what is being repaired.
const prefix = p.requires_justification.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(prefix + '\\s+(\\S[^\\n]*)', 'i');
const m = userPrompt.match(re);
if (!m || !m[1] || !m[1].trim()) continue;
}
return p;
}
export function findOverride(_userPrompt, _ruleKey, _vocab) {
return null;
}
/**
* Diagnostic variant: returns phrase object if substring matches AND rule
* applies, regardless of justification presence. Use ONLY for error-message
* generation in hooks — never to grant suppression.
*
* Fixes silent-reject bug where users see "no verification artifact" while
* having typed the override phrase but missing the justification line.
*/
export function findOverrideAttempt(userPrompt, ruleKey, vocab) {
if (!userPrompt || typeof userPrompt !== 'string') return null;
const v = vocab || loadOverrideVocab();
const lo = userPrompt.toLowerCase();
for (const p of v.phrases || []) {
if (!p.phrase || !Array.isArray(p.suppresses)) continue;
if (!lo.includes(p.phrase.toLowerCase())) continue;
if (!p.suppresses.includes(ruleKey)) continue;
return p;
}
export function findOverrideAttempt(_userPrompt, _ruleKey, _vocab) {
return null;
}
export function logHookOutcome(ruleKey, outcome, sessionId) {
try {
const f = join(runtimeDir(), 'hook-outcomes.jsonl');
+60 -77
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
@@ -25,6 +25,25 @@ import {
runtimeDir,
} from './enforce-hook-helpers.mjs';
// v4: override surface removed per spec §4.2 — stubs return null/empty
describe('v4 override stubs', () => {
it('loadOverrideVocab returns empty phrases array (stub)', () => {
_resetVocabCache();
expect(loadOverrideVocab()).toEqual({ phrases: [] });
});
it('findOverride always returns null (vocab removed in v4)', () => {
_resetVocabCache();
expect(findOverride('срочно: ремонт', 'verify-before-push')).toBe(null);
expect(findOverride('memory dump fix it now', 'memory-coverage')).toBe(null);
expect(findOverride('', 'anything')).toBe(null);
});
it('findOverrideAttempt always returns null (vocab removed in v4)', () => {
_resetVocabCache();
expect(findOverrideAttempt('срочно push it', 'verify-before-push')).toBe(null);
expect(findOverrideAttempt('', 'anything')).toBe(null);
});
});
describe('logHookOutcome', () => {
const ledgerPath = () => join(runtimeDir(), 'hook-outcomes.jsonl');
@@ -175,128 +194,92 @@ describe('lastTurnEntries / lastUserPromptText / lastAssistantText / turnToolUse
});
});
describe('loadOverrideVocab / findOverride', () => {
let tmp;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'vocab-'));
_resetVocabCache();
});
afterEach(() => {
rmSync(tmp, { recursive: true, force: true });
_resetVocabCache();
describe('loadOverrideVocab / findOverride (v4 stubs)', () => {
beforeEach(() => { _resetVocabCache(); });
afterEach(() => { _resetVocabCache(); });
it('loadOverrideVocab always returns empty phrases (stub ignores path arg)', () => {
const v = loadOverrideVocab('/any/path/vocab.json');
expect(v.phrases).toHaveLength(0);
});
it('loads vocab from explicit path', () => {
const p = join(tmp, 'vocab.json');
writeFileSync(p, JSON.stringify({
phrases: [
{ phrase: 'без скилов', suppresses: ['skill-required'] },
],
}));
const v = loadOverrideVocab(p);
expect(v.phrases).toHaveLength(1);
});
it('findOverride matches case-insensitively', () => {
it('findOverride always returns null regardless of vocab arg (stub)', () => {
const v = { phrases: [{ phrase: 'СРОЧНО', suppresses: ['verify-before-push'] }] };
expect(findOverride('очень срочно нужно', 'verify-before-push', v)).toMatchObject({ phrase: 'СРОЧНО' });
expect(findOverride('очень срочно нужно', 'verify-before-push', v)).toBeNull();
expect(findOverride('hello world', 'verify-before-push', v)).toBeNull();
});
it('findOverride returns null if rule key not in suppresses', () => {
it('findOverride returns null regardless of rule key (stub)', () => {
const v = { phrases: [{ phrase: 'без скилов', suppresses: ['skill-required'] }] };
expect(findOverride('без скилов давай', 'tdd-gate', v)).toBeNull();
expect(findOverride('без скилов давай', 'skill-required', v)).not.toBeNull();
expect(findOverride('без скилов давай', 'skill-required', v)).toBeNull();
});
it('findOverride returns null on empty prompt / vocab', () => {
it('findOverride returns null on empty prompt / vocab (unchanged)', () => {
expect(findOverride('', 'x', { phrases: [] })).toBeNull();
expect(findOverride(null, 'x', { phrases: [{ phrase: 'a', suppresses: ['x'] }] })).toBeNull();
});
it('loads default vocab file when no path given (smoke)', () => {
it('loadOverrideVocab default returns empty phrases (stub smoke)', () => {
_resetVocabCache();
const v = loadOverrideVocab();
expect(Array.isArray(v.phrases)).toBe(true);
expect(v.phrases.length).toBeGreaterThan(0);
expect(v.phrases.length).toBe(0);
});
});
describe('findOverride — requires_justification (hole 7)', () => {
describe('findOverride — requires_justification [v4: always null]', () => {
const testVocab = {
phrases: [
{
phrase: 'ремонт инфраструктуры',
suppresses: ['classifier-mismatch'],
requires_justification: 'ремонт:',
description: 'master kill — requires justification',
},
],
phrases: [{
phrase: 'ремонт инфраструктуры',
suppresses: ['classifier-mismatch'],
requires_justification: 'ремонт:',
description: 'master kill',
}],
};
it('rejects when phrase present but justification line missing (hole 7)', () => {
const r = findOverride('ремонт инфраструктуры', 'classifier-mismatch', testVocab);
expect(r).toBeNull();
it('stub: null even without justification (was null before too)', () => {
expect(findOverride('ремонт инфраструктуры', 'classifier-mismatch', testVocab)).toBeNull();
});
it('accepts when justification line provides target', () => {
const r = findOverride('ремонт инфраструктуры\nремонт: enforce-hook-helpers.mjs', 'classifier-mismatch', testVocab);
expect(r).not.toBeNull();
expect(r.phrase).toBe('ремонт инфраструктуры');
it('stub: null even with valid justification (vocab removed in v4)', () => {
expect(findOverride('ремонт инфраструктуры\nремонт: fix.mjs', 'classifier-mismatch', testVocab)).toBeNull();
});
it('rejects when justification line empty after the prefix', () => {
const r = findOverride('ремонт инфраструктуры\nремонт: ', 'classifier-mismatch', testVocab);
expect(r).toBeNull();
it('stub: null when justification empty (same as before, now via stub)', () => {
expect(findOverride('ремонт инфраструктуры\nремонт: ', 'classifier-mismatch', testVocab)).toBeNull();
});
});
describe('findOverrideAttempt — diagnostic helper (silent-reject bug fix)', () => {
describe('findOverrideAttempt [v4: always null]', () => {
const testVocab = {
phrases: [
{
phrase: 'ремонт инфраструктуры',
suppresses: ['verify-before-push', 'classifier-mismatch'],
requires_justification: 'ремонт:',
description: 'master kill — requires justification',
},
{
phrase: 'срочно',
suppresses: ['verify-before-push'],
description: 'no justification required',
},
{ phrase: 'ремонт инфраструктуры', suppresses: ['verify-before-push', 'classifier-mismatch'], requires_justification: 'ремонт:', description: 'master kill' },
{ phrase: 'срочно', suppresses: ['verify-before-push'], description: 'no justification required' },
],
};
it('returns phrase even when justification line missing (so caller can emit helpful diagnostic)', () => {
const r = findOverrideAttempt('ремонт инфраструктуры', 'verify-before-push', testVocab);
expect(r).not.toBeNull();
expect(r.phrase).toBe('ремонт инфраструктуры');
expect(r.requires_justification).toBe('ремонт:');
it('stub: null even when justification line missing (vocab removed in v4)', () => {
expect(findOverrideAttempt('ремонт инфраструктуры', 'verify-before-push', testVocab)).toBeNull();
});
it('returns phrase when justification IS provided (same behaviour as findOverride for success path)', () => {
const r = findOverrideAttempt('ремонт инфраструктуры\nремонт: observer refresh', 'verify-before-push', testVocab);
expect(r).not.toBeNull();
expect(r.phrase).toBe('ремонт инфраструктуры');
it('stub: null even when justification IS provided (vocab removed in v4)', () => {
expect(findOverrideAttempt('ремонт инфраструктуры\nремонт: observer refresh', 'verify-before-push', testVocab)).toBeNull();
});
it('returns phrase for non-justification overrides (e.g., срочно)', () => {
const r = findOverrideAttempt('срочно надо', 'verify-before-push', testVocab);
expect(r).not.toBeNull();
expect(r.phrase).toBe('срочно');
it('stub: null for срочно override (vocab removed in v4)', () => {
expect(findOverrideAttempt('срочно надо', 'verify-before-push', testVocab)).toBeNull();
});
it('returns null when phrase substring not in prompt', () => {
it('returns null when phrase substring not in prompt (still null via stub)', () => {
expect(findOverrideAttempt('hello world', 'verify-before-push', testVocab)).toBeNull();
});
it('returns null when rule key not in suppresses (phrase irrelevant)', () => {
const r = findOverrideAttempt('ремонт инфраструктуры', 'tdd-gate-other', testVocab);
expect(r).toBeNull();
it('returns null when rule key not in suppresses (still null via stub)', () => {
expect(findOverrideAttempt('ремонт инфраструктуры', 'tdd-gate-other', testVocab)).toBeNull();
});
it('returns null on empty / null prompt', () => {
it('returns null on empty / null prompt (unchanged)', () => {
expect(findOverrideAttempt('', 'verify-before-push', testVocab)).toBeNull();
expect(findOverrideAttempt(null, 'verify-before-push', testVocab)).toBeNull();
});
+16 -23
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect } from 'vitest';
import { decide, extractStagedFiles, isSecurityRelevantPath, sessionRanSemgrep } from './enforce-semgrep-security.mjs';
import { findOverride } from './enforce-hook-helpers.mjs';
@@ -148,33 +148,26 @@ describe('decide() — enforce-semgrep-security', () => {
});
});
describe('override vocab coverage', () => {
it("global override \"без скилов\" suppresses semgrep-security", () => {
const o = findOverride("без скилов", 'semgrep-security');
expect(o).toBeTruthy();
describe('override vocab coverage [v4: findOverride is stub, always null]', () => {
it("global override 'без скилов': findOverride returns null in v4 (vocab removed)", () => {
expect(findOverride('без скилов', 'semgrep-security')).toBeNull();
});
it("global override \"direct ok\" suppresses semgrep-security", () => {
const o = findOverride("direct ok", 'semgrep-security');
expect(o).toBeTruthy();
it("global override 'direct ok': findOverride returns null in v4 (vocab removed)", () => {
expect(findOverride('direct ok', 'semgrep-security')).toBeNull();
});
it("global override \"срочно\" suppresses semgrep-security", () => {
const o = findOverride("срочно", 'semgrep-security');
expect(o).toBeTruthy();
it("global override 'срочно': findOverride returns null in v4 (vocab removed)", () => {
expect(findOverride('срочно', 'semgrep-security')).toBeNull();
});
it("global override \"быстрый коммит\" suppresses semgrep-security", () => {
const o = findOverride("быстрый коммит", 'semgrep-security');
expect(o).toBeTruthy();
it("global override 'быстрый коммит': findOverride returns null in v4 (vocab removed)", () => {
expect(findOverride('быстрый коммит', 'semgrep-security')).toBeNull();
});
it("global override \"recovery\" does NOT suppress semgrep-security (git-only scope)", () => {
const o = findOverride("recovery", 'semgrep-security');
expect(o).toBeFalsy();
it("global override 'recovery': findOverride returns null in v4 (was falsy/null before too)", () => {
expect(findOverride('recovery', 'semgrep-security')).toBeNull();
});
it("global override \"memory dump\" suppresses semgrep-security", () => {
const o = findOverride("memory dump", 'semgrep-security');
expect(o).toBeTruthy();
it("global override 'memory dump': findOverride returns null in v4 (vocab removed)", () => {
expect(findOverride('memory dump', 'semgrep-security')).toBeNull();
});
it("global override \"ремонт инфраструктуры\" does NOT suppress semgrep-security (narrowed to verify-only)", () => {
const o = findOverride("ремонт инфраструктуры\nремонт: test reason", 'semgrep-security');
expect(o).toBeFalsy();
it("global override 'ремонт инфраструктуры': findOverride returns null in v4 (was also null/falsy before)", () => {
expect(findOverride('ремонт инфраструктуры\nремонт: test reason', 'semgrep-security')).toBeNull();
});
});