From 5883fc142eb41abae1112e7e64e681ce572ed2e4 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: Sun, 24 May 2026 06:45:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(brain):=20pure=20adapter=20registry=20?= =?UTF-8?q?=E2=86=92=20{classificationMap,=20dormancy}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 2 Task 2 — заменяет observer-classification-map.json и extract-node-dormancy.mjs как источник истины для missed-activation matcher. Реестр nodes.yaml становится single source. Pure module, read-only, без exec/fs (caller passes loaded registry). Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/registry-to-classification-map.mjs | 45 +++++++++++ tools/registry-to-classification-map.test.mjs | 79 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 tools/registry-to-classification-map.mjs create mode 100644 tools/registry-to-classification-map.test.mjs diff --git a/tools/registry-to-classification-map.mjs b/tools/registry-to-classification-map.mjs new file mode 100644 index 00000000..809e2cc0 --- /dev/null +++ b/tools/registry-to-classification-map.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +/** + * Pure adapter: docs/registry/nodes.yaml → {classificationMap, dormancy}. + * + * Replaces tools/observer-classification-map.json (single point of edit + * для маппинга «task_classification → recommended node ids») and + * tools/extract-node-dormancy.mjs (Tooling §4.X scraping for status). + * + * Pure / read-only. No exec, no fs (caller passes loaded registry). + * Source of truth for missed-activation detection (Pravila §16.4 v1.36). + * + * Security Guidance #40: pure parsing — no exec/execSync. + */ + +/** + * Group active-status nodes by their `classification:` trigger value. + * Returns `{ [classification]: [nodeId, ...] }`. Nodes without classification + * triggers, or non-active (dormant/deferred/historic), are excluded. + */ +export function buildClassificationMap(registry) { + const out = {}; + for (const node of registry.nodes || []) { + if (node.status !== 'active') continue; + for (const t of node.triggers || []) { + if (!t.classification) continue; + const c = t.classification; + if (!out[c]) out[c] = []; + if (!out[c].includes(node.id)) out[c].push(node.id); + } + } + return out; +} + +/** + * Build dormancy map for missed-activations consumer: id → true iff node is + * effectively unavailable (status ∈ {dormant, deferred, historic}). + * Active nodes: false. Unknown nodes: absent. + */ +export function buildDormancyMap(registry) { + const out = {}; + for (const node of registry.nodes || []) { + out[node.id] = node.status !== 'active'; + } + return out; +} diff --git a/tools/registry-to-classification-map.test.mjs b/tools/registry-to-classification-map.test.mjs new file mode 100644 index 00000000..582a92e3 --- /dev/null +++ b/tools/registry-to-classification-map.test.mjs @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs'; + +function node(id, status, classificationTriggers) { + return { + id, + status, + triggers: classificationTriggers.map(c => ({ classification: c, weight: 1.0 })), + }; +} + +const registry = { + nodes: [ + node('#11', 'active', ['refactor', 'cleanup']), + node('#12', 'active', ['refactor', 'cleanup']), + node('#17', 'dormant', ['refactor']), + node('#44', 'deferred', ['feature']), + node('#1', 'historic', ['analysis']), + node('#19', 'active', ['feature', 'planning']), + node('#99', 'active', []), // no classification triggers — ignored + ], +}; + +describe('buildClassificationMap', () => { + it('groups active nodes by classification', () => { + const map = buildClassificationMap(registry); + expect(map.refactor.sort()).toEqual(['#11', '#12']); + expect(map.feature).toEqual(['#19']); + expect(map.planning).toEqual(['#19']); + expect(map.cleanup.sort()).toEqual(['#11', '#12']); + }); + + it('excludes dormant nodes', () => { + const map = buildClassificationMap(registry); + expect(map.refactor).not.toContain('#17'); + }); + + it('excludes deferred nodes', () => { + const map = buildClassificationMap(registry); + expect(map.feature || []).not.toContain('#44'); + }); + + it('excludes historic nodes', () => { + const map = buildClassificationMap(registry); + expect(map.analysis).toBeUndefined(); + }); + + it('ignores nodes with no classification triggers', () => { + const map = buildClassificationMap(registry); + expect(Object.values(map).flat()).not.toContain('#99'); + }); + + it('returns an empty object on an empty registry', () => { + expect(buildClassificationMap({ nodes: [] })).toEqual({}); + }); +}); + +describe('buildDormancyMap', () => { + it('marks dormant nodes true', () => { + const dorm = buildDormancyMap(registry); + expect(dorm['#17']).toBe(true); + }); + + it('marks deferred nodes true', () => { + const dorm = buildDormancyMap(registry); + expect(dorm['#44']).toBe(true); + }); + + it('marks historic nodes true', () => { + const dorm = buildDormancyMap(registry); + expect(dorm['#1']).toBe(true); + }); + + it('marks active nodes false', () => { + const dorm = buildDormancyMap(registry); + expect(dorm['#11']).toBe(false); + expect(dorm['#19']).toBe(false); + }); +});