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