diff --git a/tools/enforce-normative-content-rules.mjs b/tools/enforce-normative-content-rules.mjs index 6316c9bd..4065982a 100644 --- a/tools/enforce-normative-content-rules.mjs +++ b/tools/enforce-normative-content-rules.mjs @@ -40,6 +40,49 @@ export function isProtectiveNormativePath(filePath) { return PROTECTIVE_NORMATIVE_PATTERNS.some((re) => re.test(n)); } +// §6 build-loop (SE-D): исходники машин М1–М6 — правка этих файлов меняет САМИ правила/дисциплину. +// Вне запечатанного плана → ЗАКОН (ad-hoc самомодификация требует escape); под планом → КАРТА +// (управляется стеной М2 + content-floor + TDD — цикл сборки не клинит). +const DISCIPLINE_SOURCE_RE = /(^|\/)tools\/(enforce-|judge-|floor-|escape-grant|action-journal|receipt-|shell-content-rules|plan-lock|classify-destructive|path-normalization)[^/]*\.mjs$/; +export function isDisciplineSourcePath(filePath) { + if (typeof filePath !== 'string') return false; + return DISCIPLINE_SOURCE_RE.test(filePath.replace(/\\/g, '/')); +} + +// §6 контент-детектор «правит секцию правил/дисциплины» (сомнение → ЗАКОН): маркеры правки +// самих машин/правил в теле (имя enforce-/judge-/floor- + правило/блок/снять/disable/gate). +const LAW_CONTENT_RE = /(enforce-|judge-|floor-)[a-z-]+[^.\n]{0,60}(?:правил|правит|блок|снять|disable|переопредел|gate)/iu; +export function contentTouchesLaw(content) { + return typeof content === 'string' && LAW_CONTENT_RE.test(content); +} + +/** + * §6 классификатор КАРТА/ЗАКОН. КАРТА (operational) — CLAUDE.md/MEMORY.md/memory: контроллер + * ведёт свободно (claude-md-management). ЗАКОН (normative) — Pravila/PSR/Tooling + дисциплинарный + * исходник ВНЕ плана + контент, правящий правила: требует escape владельца (М6). build-loop: + * дисциплинарный исходник ПОД запечатанным планом (sealedPlanCoversEdit) → КАРТА. Сомнение → ЗАКОН. + * @returns {{kind:'CARD'|'LAW', reason:string}} + */ +export function classifyNormative(filePath, { content = '', sealedPlanCoversEdit = false } = {}) { + const n = String(filePath || '').replace(/\\/g, '/'); + // ЗАКОН-пути: Pravila/PSR/Tooling (нормативные документы-правила). + if (/(^|\/)docs\/(Pravila_|Plugin_stack_rules_|Tooling_)[^/]*\.md$/.test(n)) { + return { kind: 'LAW', reason: 'normative rules document (Pravila/PSR/Tooling)' }; + } + // Дисциплинарный исходник: build-loop различитель (SE-D). + if (isDisciplineSourcePath(n)) { + return sealedPlanCoversEdit + ? { kind: 'CARD', reason: 'discipline source under sealed plan (build-loop)' } + : { kind: 'LAW', reason: 'discipline source edited ad-hoc (outside sealed plan)' }; + } + // Контент правит правила/дисциплину машин → ЗАКОН (сомнение → ЗАКОН). + if (contentTouchesLaw(content)) { + return { kind: 'LAW', reason: 'content edits a rules/discipline section' }; + } + // Иначе — операционная КАРТА (CLAUDE.md/MEMORY.md/memory/заметки). + return { kind: 'CARD', reason: 'operational map (CLAUDE.md/MEMORY.md/memory)' }; +} + /** Extract the new content a mutating tool would write. */ export function extractWrittenContent(toolName, toolInput) { const i = toolInput || {}; diff --git a/tools/enforce-normative-content-rules.test.mjs b/tools/enforce-normative-content-rules.test.mjs index c568160f..9f3dffa7 100644 --- a/tools/enforce-normative-content-rules.test.mjs +++ b/tools/enforce-normative-content-rules.test.mjs @@ -209,3 +209,40 @@ describe('decide 7.2 (H3): деградация судьи fail-CLOSE тольк expect(r.reason).toMatch(/recovery/i); }); }); + +import { classifyNormative, isDisciplineSourcePath } from './enforce-normative-content-rules.mjs'; + +describe('isDisciplineSourcePath — исходники машин М1–М6', () => { + for (const p of ['tools/enforce-floor.mjs', 'tools/judge-orchestrator.mjs', 'tools/floor-signer.mjs', 'tools/escape-grant.mjs', 'tools/action-journal.mjs', 'tools/shell-content-rules.mjs', 'tools/plan-lock.mjs']) { + it(`${p} → true`, () => expect(isDisciplineSourcePath(p)).toBe(true)); + } + for (const p of ['tools/cost-aggregator.mjs', 'tools/observer-routing-detector.mjs', 'app/Models/User.php', 'CLAUDE.md']) { + it(`${p} → false`, () => expect(isDisciplineSourcePath(p)).toBe(false)); + } +}); + +describe('classifyNormative (§6 КАРТА/ЗАКОН + build-loop SE-D)', () => { + it('CLAUDE.md / MEMORY.md / memory → CARD (operational, низкий порог)', () => { + expect(classifyNormative('CLAUDE.md').kind).toBe('CARD'); + expect(classifyNormative('MEMORY.md').kind).toBe('CARD'); + expect(classifyNormative('memory/feedback_x.md').kind).toBe('CARD'); + }); + it('Pravila / PSR / Tooling → LAW (правила, что связывают контроллера)', () => { + expect(classifyNormative('docs/Pravila_raboty_Claude_v1_1.md').kind).toBe('LAW'); + expect(classifyNormative('docs/Plugin_stack_rules_v1.md').kind).toBe('LAW'); + expect(classifyNormative('docs/Tooling_v8_3.md').kind).toBe('LAW'); + }); + it('дисциплинарный исходник ВНЕ плана → LAW (ad-hoc самомодификация)', () => { + expect(classifyNormative('tools/enforce-floor.mjs', { sealedPlanCoversEdit: false }).kind).toBe('LAW'); + }); + it('дисциплинарный исходник ПОД запечатанным планом → CARD (build-loop, SE-D)', () => { + expect(classifyNormative('tools/enforce-floor.mjs', { sealedPlanCoversEdit: true }).kind).toBe('CARD'); + }); + it('контент правит секцию правил/дисциплины в любом файле → LAW (сомнение → ЗАКОН)', () => { + const content = 'обнови правило enforce-floor: блок снять при флаге'; + expect(classifyNormative('memory/x.md', { content }).kind).toBe('LAW'); + }); + it('обычный КАРТА-контент без правило-маркеров → CARD', () => { + expect(classifyNormative('memory/x.md', { content: 'урок: rebase ломается на observer-файлах' }).kind).toBe('CARD'); + }); +});