feat(observer): recommended-node resolver for direct episodes

Mirrors missed-activations dormancy logic (id === false strict).
First live recommended node from classification-map, else null.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-23 13:22:55 +03:00
parent 7fdf0ba971
commit 3ecb0134bd
3 changed files with 69 additions and 3 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-23T10:16:44.994Z
Last updated: 2026-05-23T10:20:01.355Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,12 +8,12 @@ Last updated: 2026-05-23T10:16:44.994Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ⚠️ | 143 episode(s) this month · Stop-hook + post-commit OK · 21 missed activation(s) — see /brain-retro |
| C5 Observer-coverage | ⚠️ | 144 episode(s) this month · Stop-hook + post-commit OK · 21 missed activation(s) — see /brain-retro |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Метрики (информационные, не алерты)
- Observer evidence: 143 episodes this month, 0 observer_error markers, 64 PII matches before filter
- Observer evidence: 144 episodes this month, 0 observer_error markers, 67 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 5
- Last /brain-retro: 0 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 21. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
/**
* Recommended-node resolver for direct episodes.
* Pure — read-only, no exec, no fs (Security Guidance #40).
*
* For an episode classified as `taskClassification` with node_chosen='direct',
* return the first live (non-dormant) recommended node ID from the
* classification map. Mirrors missed-activations.mjs dormancy logic:
* dormancy[id] === false strictly (missing/true → not live).
*
* Per spec: docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md
*/
export function recommendNode(taskClassification, classificationMap, dormancy) {
if (!taskClassification || !classificationMap || !dormancy) return null;
const recommended = classificationMap[taskClassification];
if (!Array.isArray(recommended) || recommended.length === 0) return null;
for (const id of recommended) {
if (dormancy[id] === false) return id;
}
return null;
}
+44
View File
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest';
import { recommendNode } from './observer-recommended-node.mjs';
const MAP = {
feature: ['#19'],
refactor: ['#11', '#12', '#43'],
question: [],
other: [],
};
describe('recommendNode', () => {
it('returns first live node ID for a known classification', () => {
expect(recommendNode('feature', MAP, { '#19': false })).toBe('#19');
});
it('skips dormant first node, returns next live', () => {
expect(recommendNode('refactor', MAP, { '#11': true, '#12': false, '#43': false })).toBe('#12');
});
it('returns null when all recommended nodes are dormant', () => {
expect(recommendNode('refactor', MAP, { '#11': true, '#12': true, '#43': true })).toBeNull();
});
it('returns null for classification absent from map', () => {
expect(recommendNode('nonexistent', MAP, {})).toBeNull();
});
it('returns null for empty-array classification (question/memory-sync)', () => {
expect(recommendNode('question', MAP, {})).toBeNull();
expect(recommendNode('other', MAP, {})).toBeNull();
});
it('treats missing dormancy entry as live (defensive, parity with missed-activations)', () => {
// missed-activations uses dormancy[id] === false; recommendNode mirrors:
// unknown/missing → not live (paranoid — only positive false counts as live).
expect(recommendNode('feature', MAP, {})).toBeNull();
});
it('handles null/undefined inputs without throwing', () => {
expect(recommendNode(null, MAP, {})).toBeNull();
expect(recommendNode('feature', null, {})).toBeNull();
expect(recommendNode('feature', MAP, null)).toBeNull();
});
});