feat(m7-phase5): classifyNormative — детерминированный КАРТА/ЗАКОН + build-loop SE-D (§6)

Новые чистые экспорты в enforce-normative-content-rules: isDisciplineSourcePath (исходники
машин М1–М6 по префиксам enforce-/judge-/floor-/escape-grant/action-journal/receipt-/
shell-content-rules/plan-lock/classify-destructive/path-normalization), contentTouchesLaw
(контент правит правила/дисциплину), classifyNormative → {kind:'CARD'|'LAW'}. КАРТА =
CLAUDE.md/MEMORY.md/memory; ЗАКОН = Pravila/PSR/Tooling + дисциплинарный исходник ВНЕ плана +
контент-правка правил; build-loop: дисциплинарный исходник ПОД sealed-планом → КАРТА; сомнение
→ ЗАКОН. Тотален (null guard). Интеграция в decide — Task 2. 44/44 GREEN.
This commit is contained in:
Дмитрий
2026-06-08 12:46:46 +03:00
parent f9a4b10d1f
commit 2d2f3fc591
2 changed files with 80 additions and 0 deletions
+43
View File
@@ -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 || {};
@@ -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');
});
});