From 3ecb0134bd7634b6bcc2fd273e34df111fdfb42d 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: Sat, 23 May 2026 13:22:55 +0300 Subject: [PATCH] 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) --- docs/observer/STATUS.md | 6 ++-- tools/observer-recommended-node.mjs | 22 ++++++++++++ tools/observer-recommended-node.test.mjs | 44 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tools/observer-recommended-node.mjs create mode 100644 tools/observer-recommended-node.test.mjs diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 8ca76bab..ff3f43b1 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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). diff --git a/tools/observer-recommended-node.mjs b/tools/observer-recommended-node.mjs new file mode 100644 index 00000000..f1177a8c --- /dev/null +++ b/tools/observer-recommended-node.mjs @@ -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; +} diff --git a/tools/observer-recommended-node.test.mjs b/tools/observer-recommended-node.test.mjs new file mode 100644 index 00000000..39e5e32b --- /dev/null +++ b/tools/observer-recommended-node.test.mjs @@ -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(); + }); +});