feat(brain): pure adapter registry → {classificationMap, dormancy}

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) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-24 06:45:27 +03:00
parent e59dbe03e4
commit 5883fc142e
2 changed files with 124 additions and 0 deletions
+45
View File
@@ -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;
}
@@ -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);
});
});