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:
+19
-19
@@ -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 |
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user