From d3e192abf144c07c486cf5280bb6d11426281bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 30 May 2026 05:55:46 +0300 Subject: [PATCH] =?UTF-8?q?refactor(router-gate-v4):=20stub=20override=20h?= =?UTF-8?q?elpers=20(universal=20vocab=20removed=20per=20spec=20=C2=A74.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/observer/STATUS.md | 38 +++---- tools/enforce-hook-helpers.mjs | 56 ++-------- tools/enforce-hook-helpers.test.mjs | 137 +++++++++++------------- tools/enforce-semgrep-security.test.mjs | 39 +++---- 4 files changed, 103 insertions(+), 167 deletions(-) diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index ccdac2c9..2140d251 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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 | diff --git a/tools/enforce-hook-helpers.mjs b/tools/enforce-hook-helpers.mjs index 289e2bc3..cae1c94a 100644 --- a/tools/enforce-hook-helpers.mjs +++ b/tools/enforce-hook-helpers.mjs @@ -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 " " - // 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'); diff --git a/tools/enforce-hook-helpers.test.mjs b/tools/enforce-hook-helpers.test.mjs index 38967eec..c8dd465f 100644 --- a/tools/enforce-hook-helpers.test.mjs +++ b/tools/enforce-hook-helpers.test.mjs @@ -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(); }); diff --git a/tools/enforce-semgrep-security.test.mjs b/tools/enforce-semgrep-security.test.mjs index b2b48da4..e921c0c5 100644 --- a/tools/enforce-semgrep-security.test.mjs +++ b/tools/enforce-semgrep-security.test.mjs @@ -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(); }); });