From 6dec34403ff6a6da46e1b616bf514b49ed9c9632 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: Thu, 21 May 2026 08:24:57 +0300 Subject: [PATCH] feat(observer): node-dormancy extractor + initial JSON snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-signal availability check: dormant=true OR boundaries contains DEFERRED. Treats #17 (Tooling-marked) and #44/#50/#54/#67 (DEFERRED in boundaries) uniformly as unavailable. Tooling Прил.Н unmodified — semantics preserved. 7 vitest cases (basic, multi-row, DEFERRED-fallback, boundary check). Initial JSON: 67 nodes, 6 unavailable. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/observer/STATUS.md | 6 +-- tools/.node-dormancy.json | 69 ++++++++++++++++++++++++++++ tools/extract-node-dormancy.mjs | 39 ++++++++++++++++ tools/extract-node-dormancy.test.mjs | 53 +++++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 tools/.node-dormancy.json create mode 100644 tools/extract-node-dormancy.mjs create mode 100644 tools/extract-node-dormancy.test.mjs diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 2cf07844..963f68dd 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-21T01:53:48.034Z +Last updated: 2026-05-21T05:24:48.415Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,12 +8,12 @@ Last updated: 2026-05-21T01:53:48.034Z | 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 | ⚠️ | 16 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) | +| C5 Observer-coverage | ⚠️ | 39 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) | | C6 Chain map sync | ✅ | [chain-map-checker] OK — 14 chains in sync | ## Метрики (информационные, не алерты) -- Observer evidence: 16 episodes this month, 0 observer_error markers, 0 PII matches before filter +- Observer evidence: 39 episodes this month, 0 observer_error markers, 0 PII matches before filter - Legacy v1 episodes (not in factor analysis): 5 - Last /brain-retro: 2 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). diff --git a/tools/.node-dormancy.json b/tools/.node-dormancy.json new file mode 100644 index 00000000..c452859b --- /dev/null +++ b/tools/.node-dormancy.json @@ -0,0 +1,69 @@ +{ + "#1": true, + "#2": false, + "#3": false, + "#4": false, + "#5": false, + "#6": false, + "#7": false, + "#8": false, + "#9": false, + "#10": false, + "#11": false, + "#12": false, + "#13": false, + "#14": false, + "#15": false, + "#16": false, + "#17": true, + "#18": false, + "#19": false, + "#20": false, + "#21": false, + "#22": false, + "#23": false, + "#24": false, + "#30": false, + "#31": false, + "#32": false, + "#33": false, + "#34": false, + "#35": false, + "#36": false, + "#37": false, + "#38": false, + "#39": false, + "#40": false, + "#41": false, + "#42": false, + "#43": false, + "#44": true, + "#45": false, + "#46": false, + "#47": false, + "#48": false, + "#49": false, + "#50": true, + "#51": false, + "#52": false, + "#53": false, + "#54": true, + "#55": false, + "#56": false, + "#57": false, + "#58": false, + "#59": false, + "#60": false, + "#61": false, + "#62": false, + "#63": false, + "#64": false, + "#65": false, + "#66": false, + "#67": true, + "#25": false, + "#26": false, + "#27": false, + "#28": false, + "#29": false +} diff --git a/tools/extract-node-dormancy.mjs b/tools/extract-node-dormancy.mjs new file mode 100644 index 00000000..57e282de --- /dev/null +++ b/tools/extract-node-dormancy.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +/** + * Tooling Прил.Н dormancy extractor — emits {id: unavailable_bool} JSON for + * the missed-activation matcher (Pravila §16.4 conditional rule). + * + * Two signals (either is sufficient) treat a node as effectively unavailable: + * 1. `dormant: true` — Tooling-marked permanent dormancy (e.g. #17 pg_partman, + * native Windows-PG cannot load the extension). + * 2. `boundaries` column contains the word DEFERRED — node is registered + * but not active (e.g. #44 Figma MCP "DEFERRED — нет Figma-аккаунта", + * #50 Jupyter MCP, #54 n8n-mcp). The output key is still named "dormant" + * for consumer simplicity — semantics: "node cannot be activated right + * now, exclude from missed-activation counts". + * + * Parses 9-attribute table rows; ignores headers/separators/templates. + * + * Security Guidance #40: pure parsing — no exec/execSync. + */ +import { readFileSync, writeFileSync } from 'fs'; + +const ROW_RE = /^\|\s*#(\d+)\s*\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|([^|]+)\|\s*(true|false)\s*\|[^|]+\|$/gm; + +export function extractDormancy(md) { + const out = {}; + for (const m of md.matchAll(ROW_RE)) { + const id = `#${m[1]}`; + const boundaries = m[2]; + const tooledDormant = m[3] === 'true'; + out[id] = tooledDormant || /\bDEFERRED\b/.test(boundaries); + } + return out; +} + +if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/extract-node-dormancy.mjs')) { + const src = readFileSync('docs/Tooling_v8_3.md', 'utf-8'); + const dormancy = extractDormancy(src); + writeFileSync('tools/.node-dormancy.json', JSON.stringify(dormancy, null, 2) + '\n'); + console.log(`[extract-node-dormancy] OK — ${Object.keys(dormancy).length} nodes`); +} diff --git a/tools/extract-node-dormancy.test.mjs b/tools/extract-node-dormancy.test.mjs new file mode 100644 index 00000000..58c8b0c4 --- /dev/null +++ b/tools/extract-node-dormancy.test.mjs @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; +import { extractDormancy } from './extract-node-dormancy.mjs'; + +describe('extractDormancy', () => { + it('returns false for a live row (dormant=false, no DEFERRED in boundaries)', () => { + const md = [ + '#### #10 Laravel Boost', + '', + '**Атрибуты:**', + '', + '| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched |', + '|---|---|---|---|---|---|---|---|---|', + '| #10 | Laravel Boost | composer-dep | 1 | — | «SQL, Eloquent» | replaces #1 PG MCP | false | 2026-05-19 |', + ].join('\n'); + expect(extractDormancy(md)).toEqual({ '#10': false }); + }); + + it('returns true when Tooling marks dormant=true', () => { + const md = '| #17 | pg_partman | binary-dep | 1 | — | «partition mgmt» | none | true | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({ '#17': true }); + }); + + it('returns true when boundaries contains DEFERRED (even if dormant=false)', () => { + const md = '| #44 | Figma MCP | mcp | off-phase | design-tooling | «figma extract» | DEFERRED — нет Figma-аккаунта | false | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({ '#44': true }); + }); + + it('handles multiple nodes in one pass (mixed signals)', () => { + const md = [ + '| #44 | Figma MCP | mcp | off-phase | design-tooling | «figma extract» | DEFERRED — нет Figma | false | 2026-05-17 |', + '| #45 | Universal Icons MCP | mcp | off-phase | design-tooling | «svg search» | non-Lucide | false | 2026-05-17 |', + ].join('\n'); + expect(extractDormancy(md)).toEqual({ '#44': true, '#45': false }); + }); + + it('ignores header/separator rows', () => { + const md = [ + '| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched |', + '|---|---|---|---|---|---|---|---|---|', + ].join('\n'); + expect(extractDormancy(md)).toEqual({}); + }); + + it('ignores non-numeric ids (template placeholders)', () => { + const md = '| #NN | | | | | «» | | false | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({}); + }); + + it('does NOT match the word DEFERRED inside a longer token (boundary check)', () => { + const md = '| #99 | fake | mcp | off | tooling | «t» | NODEFERREDX prefix | false | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({ '#99': false }); + }); +});