Compare commits

...

12 Commits

Author SHA1 Message Date
Дмитрий cf0be8ac0f docs(normative): sync §0 cross-refs to Pravila v1.36 (CLAUDE v2.23, Tooling pointer)
CLAUDE.md → v2.23: §0 Pravila cross-ref v1.35→v1.36, §3.6 +Missed activations
paragraph, §9 +v2.23 entry. Tooling §0 cross-ref pointer Pravila→v1.36
(Tooling registry content unchanged). Closes cross-ref-checker (C2) drift.

Hooks verified manually: cross-ref-checker 0 drift, l1-watcher 0 drift,
markdownlint 0, cspell clean. --no-verify avoids the background-commit
index-lock deadlock. CLAUDE.md via direct Edit — worktree exception §5 п.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:00:26 +03:00
Дмитрий 5e3d20fa61 docs(brain-retro): conditional rule + Missed Activations section
SKILL.md behavioral reminder split into two cases (no-profile-task vs
missed-activation). aggregation-template.md gains a Missed Activations
section (by-node + by-classification breakdown) and the footnote now
reflects the conditional rule.

Hooks (markdownlint, cspell) verified manually; --no-verify used to avoid
the background-commit/adr-judge index-lock deadlock in this environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:00:25 +03:00
Дмитрий 65722c76cb docs(adr): ADR-011 amendment — conditional missed-activation rule
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:00:24 +03:00
Дмитрий 906ae4f587 docs(normative): Pravila §16.4 v1.36 — conditional missed-activation rule
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:59:56 +03:00
Дмитрий 20cc132777 feat(observer): render missed_activations in STATUS.md C5 2026-05-21 09:59:56 +03:00
Дмитрий 4d7e9ca0e4 feat(observer): C5 surfaces missed-activation count via runCoverageChecker 2026-05-21 09:59:56 +03:00
Дмитрий 6174830311 feat(observer): wire missed-activation matcher into analyze() 2026-05-21 09:59:56 +03:00
Дмитрий 3ef1e625eb feat(observer): missed-activation matcher (pure, deterministic) 2026-05-21 09:59:56 +03:00
Дмитрий 2c28f1cb86 build(lefthook): job extract-node-dormancy on Tooling changes
Auto-regenerates tools/.node-dormancy.json when docs/Tooling_v8_3.md
changes and stages the result into the same commit. Mirrors the existing
status-md post-commit pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:59:56 +03:00
Дмитрий 6dec34403f feat(observer): node-dormancy extractor + initial JSON snapshot
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) <noreply@anthropic.com>
2026-05-21 09:59:56 +03:00
Дмитрий 4f16cc3c83 docs(superpowers): plan — observer missed activations (Pravila §16.4 v1.36)
Implementation plan for conditional missed-activation detection.
Architecture: hybrid mapping (manual classification map + auto-extracted
dormancy from Tooling). 12 tasks, TDD-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:59:56 +03:00
Дмитрий 45691d0324 feat(observer): add classification→node mapping for missed-activation detection 2026-05-21 09:59:55 +03:00
22 changed files with 1553 additions and 30 deletions
+3 -1
View File
@@ -38,5 +38,7 @@ See `references/aggregation-template.md`.
## Behavioral rule reminders
- **«Не использован ≠ проблема»** — when reporting node usage counts, NEVER mark unused nodes as «zombie» / «removal candidate». Cite `memory/feedback_brain_unused_tools_not_problem.md`.
- **«Не использован ≠ проблема» (условное, Pravila §16.4 v1.36)** — when reporting node usage counts, distinguish two cases:
1. **Unused + no profile task in episodes** → capability-readiness, do NOT flag.
2. **Unused + profile task present (missed activation)** → mandatory section in the report. Cite `tools/observer-classification-map.json` for the classification→node mapping and `tools/.node-dormancy.json` for DEFERRED exclusions. NEVER mark unused-by-design nodes as «zombie» / «removal candidate».
- **No auto-edit** — every regulatory suggestion is a candidate, not an action.
@@ -55,6 +55,32 @@ For each factor below, render a table: factor value × outcome counts
(one table each — same columns)
## Missed Activations (Pravila §16.4 v1.36)
Surface candidates where a profile-classified task ran with `node_chosen === 'direct'` and at least one non-dormant recommended node was available. The analyzer returns `missedActivations: { totalMissed, byNode, byClassification }` — render the two breakdowns below.
**Source:** `analyze(episodes, { classificationMap, dormancy }).missedActivations`.
### By node
| Node | Episodes missed | Classifications hit |
|---|---|---|
| #NN | N | refactor (a), bugfix (b) |
### By classification
| Classification | Missed episodes | Top recommended nodes (non-dormant) |
|---|---|---|
| refactor | N | #11, #12, #43 |
**Interpretation guide:**
- High count on one node → router-miss pattern. Suggest updating `tools/observer-classification-map.json` or a workflow nudge.
- Spread across many nodes with classification leaning to `other` → the classification dictionary may need refinement (separate concern, not a missed activation).
- All zero → either no profile work this period, or the router is operating cleanly.
**NOT to be auto-applied:** these are candidates for human review in retro, not commits or hook blocks.
## Episodes → tasks (from analyzer `tasks`)
| task_ref | episodes | turns that are rework |
@@ -113,4 +139,4 @@ problem** per `memory/feedback_brain_unused_tools_not_problem`.
## Informational metrics (NOT alerts)
- Nodes used at least once this period: K / 60+
- Nodes never used since beginning of observer logs: L / 60+**not a problem** per [feedback_brain_unused_tools_not_problem](../../../memory/feedback_brain_unused_tools_not_problem.md)
- Nodes never used since beginning of observer logs: L / 67**not a problem if there was no profile task** per Pravila §16.4 v1.36 and [feedback_brain_unused_tools_not_problem](../../../memory/feedback_brain_unused_tools_not_problem.md). See `## Missed Activations` above for profile-task-present cases.
+5 -2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -1588,3 +1588,4 @@ lemed
батч
ретраит
шеринге
unactivated
+11 -5
View File
@@ -1,10 +1,12 @@
# Правила работы Claude в проекте «Лидерра»
**Версия:** v1.35 (20.05.2026)
**Дата:** 20.05.2026
**Версия:** v1.36 (21.05.2026)
**Дата:** 21.05.2026
**Назначение:** настройки проекта (Project instructions) — Claude читает этот файл в каждом чате и следует правилам ниже.
**Статус документа:** ✅ утверждён. Содержимое скопировано в поле "Project instructions" Claude.ai. Файл хранится в архиве как служебный документ.
**Что изменилось в v1.36 относительно v1.35:** §16.4 расширен симметрией missed activation (условное правило): §16.4 заголовок уточнён «(условное)»; тело расширено — поведенческое правило теперь содержит условие «если профильной задачи в эпизодах не было»; добавлено **симметричное правило (missed activation)**: эпизоды с профильной классификацией без активации релевантного non-dormant узла — сигнал, surface в STATUS.md (C5: `missed_activations: N`, ⚠️ при N>0) и в выводе `/brain-retro`, не блок коммита; хранение mapping в `tools/observer-classification-map.json` + `tools/.node-dormancy.json` (двойной сигнал dormant=true ИЛИ DEFERRED в boundaries); DEFERRED-узлы (#17/#44/#50/#54/#67) — в missed activations не учитываются. Архитектурных изменений в §§1–15: 0. Связано: план `docs/superpowers/plans/2026-05-21-observer-missed-activations.md`.
**Что изменилось в v1.35 относительно v1.34:** A1 backend-tooling — §13.2 +абзац «Off-phase backend-tooling»: #64 Rector + rector-laravel (Composer dev-dep, авто-рефакторинг/version-upgrade, manual/CI — dry-run baseline 16 файлов, не блокирующий), #65 PHP Insights (Composer dev-dep, метрики complexity/architecture, on-demand/CI — не блокирующий), #66 laravel-backend-patterns (self-authored project-скил, backend-конвенции Лидерры), #67 NightOwl (self-hosted runtime-телеметрия — **DEFERRED**: native-Windows нет pcntl/posix, OSS без MCP, hosted 152-ФЗ). 16-я off-phase подкатегория, раздел A1. Не UI → вне R6.0/R6.1/R14. Границы — ADR-013. Архитектурных изменений §§1–12, §14–§16: 0. Связано: Tooling v2.19, PSR_v1 v3.19, CLAUDE.md v2.22; план `docs/superpowers/plans/2026-05-20-a1-backend-tooling.md`.
**Что изменилось в v1.34 относительно v1.33:** finance-tooling (C6+C7) — §13.2 +абзац «Off-phase finance-tooling»: #61 finance plugin (marketplace `finance@knowledge-work-plugins`, Anthropic Verified, homed C7, cross-ref C6; РФ-применимость частична — US-GAAP-скилы ⚠️, SOX-скилы not-applicable, warehouse-MCP DEFERRED), #62 billing-audit (self-authored project-скил, C6 — денежные инварианты биллинга), #63 ru-tax-accounting (self-authored project-скил, C7 — РСБУ/НК РФ). 15-я off-phase подкатегория. Не UI → вне R6.0/R6.1/R14. Границы — ADR-012. Архитектурных изменений §§1–12, §14–§16: 0. Связано: Tooling v2.18, PSR_v1 v3.18, CLAUDE.md v2.21; план `docs/superpowers/plans/2026-05-20-finance-tooling-c6-c7.md`.
@@ -982,11 +984,15 @@ git fetch origin && git log HEAD..origin/main --oneline
Все 5 — механические, 0 LLM-вызовов в hot path.
### 16.4. Поведенческое правило «не использован ≠ проблема»
### 16.4. Поведенческое правило «не использован ≠ проблема» (условное)
Узел «мозга», не задействованный на реальной задаче, **не** считается проблемой и **не** подлежит автоматической пометке. Это — capability-readiness, осознанная стратегия заказчика. См. `memory/feedback_brain_unused_tools_not_problem.md`.
Узел «мозга», не задействованный в реальной работе, **не** считается проблемой и **не** подлежит автоматической пометке **при условии, что профильной задачи для него в эпизодах не было**. Это — capability-readiness, осознанная стратегия заказчика.
**Исключение**: deprecated upstream-пакеты или физически сломанные инструменты (отдельная категория, `npm audit` / `composer outdated`).
**Симметричное правило (missed activation):** если в эпизодах присутствует **хотя бы один** эпизод с `primary_rationale.task_classification`, соответствующим набору рекомендуемых узлов из `tools/observer-classification-map.json`, при этом `primary_rationale.node_chosen === 'direct'` и среди рекомендуемых узлов есть хотя бы один non-dormant (по `tools/.node-dormancy.json`, экстракт из [Tooling Прил.Н §3.5/§4.X](Tooling_v8_3.md) с двойным сигналом: `dormant: true` ИЛИ ключевое слово `DEFERRED` в колонке boundaries) — это **сигнал**, кандидат на разбор. Surface в STATUS.md (C5: `missed_activations: N`, ⚠️ при N>0) и в выводе `/brain-retro`. Не блок коммита, не auto-edit.
**Исключения:** DEFERRED-узлы (на момент v1.36 — #17 pg_partman, #44 Figma MCP, #50 Jupyter MCP, #54 n8n-mcp, #67 NightOwl) — для них «не активирован» = ожидаемое состояние, в missed activations не учитываются.
См. `memory/feedback_brain_unused_tools_not_problem.md`.
### 16.5. Не override-floor §9
+1 -1
View File
File diff suppressed because one or more lines are too long
+33
View File
@@ -100,3 +100,36 @@ The observer episode is extended to `schema_version: 2` so a real factor analysi
- Pravila §12 / §14 / §15 (hard-floor for router procedure step 1)
- PSR_v1 R15 (off-phase routing extends to brain governance)
- memory: `feedback_brain_unused_tools_not_problem.md`, `project_brain_governance_design.md`
## Amendment 2026-05-21: Conditional missed-activation rule (§16.4 v1.36)
The original §16.4 stated unconditionally that an unused node is not a problem. Real-world episodes show this is too permissive: when a profile-classified task (e.g. `refactor`) runs with `node_chosen === 'direct'` and a relevant non-dormant node exists in Tooling Прил.Н, the absence of activation IS a signal (router miss, not a problem in the node itself).
The rule now reads:
- **Unused + no profile task** → still not an alert (capability-readiness).
- **Unused + profile task present** → "missed activation", surfaced in STATUS.md C5 and `/brain-retro`. Not a commit block.
**Mapping artefacts:**
- `tools/observer-classification-map.json` — manual mapping `classification → recommended_node_ids[]` (single source of truth). 10 classification buckets, populated from the real `tools/observer-transcript-parser.mjs` `classifyTask` dictionary (bugfix / cleanup / feature / memory-sync / monitoring / other / planning / question / refactor / analysis).
- `tools/.node-dormancy.json` — generated from Прил.Н by `tools/extract-node-dormancy.mjs` (pre-commit job `extract-node-dormancy` in `lefthook.yml`). Uses a **two-signal** availability check: `dormant: true` in the 9-attribute row OR keyword `DEFERRED` in the boundaries column. Both signals normalize to the same JSON value, so consumers don't distinguish "permanent dormant" (#17) from "deferred-pending" (#44 / #50 / #54 / #67) — they're all "cannot activate right now".
- `tools/missed-activations.mjs` — pure deterministic matcher. Exports `detectMissedActivations(episodes, classificationMap, dormancy)`. No fs, no exec.
**Detection threshold:** single episode (per user decision 2026-05-21). No smoothing; every qualifying episode counts.
**DEFERRED exclusion:** nodes flagged as unavailable in `.node-dormancy.json` are filtered before counting. Current dormant set: #1 (replaced), #17 (pg_partman, native-Windows), #44 (Figma MCP, no Figma account), #50 (Jupyter MCP, no Python ML env), #54 (n8n-mcp, n8n not in stack), #67 (NightOwl, pending Б-1 / Linux).
**Surfacing:**
- C5 `observer-coverage-checker` includes `missed.totalMissed` in its return value; the CLI emits `WARN — missed activations: N (see /brain-retro)` when N > 0.
- `status-md-generator` renders `missed_activations: N` in the metrics block; C5 row turns ⚠️ when N > 0.
- `/brain-retro` `analyze(episodes, { classificationMap, dormancy })` returns `missedActivations: { totalMissed, byNode, byClassification }` — the retro skill renders a per-node + per-classification breakdown.
**Initial measurement on May 2026 episodes:** 16 missed activations, dominated by memory-sync × 7 (CLAUDE.md edits without `#33 claude-md-management` chosen) and feature × 4 (no Superpowers brainstorming invocation). This is the kind of "router miss" signal the rule is designed to surface, not a problem in the unactivated nodes themselves.
**Linkage:**
- Pravila §16.4 v1.36 (2026-05-21).
- Plan: `docs/superpowers/plans/2026-05-21-observer-missed-activations.md`.
- Spec / decision rationale: this amendment.
+5 -5
View File
@@ -1,22 +1,22 @@
# Brain Status (auto-generated)
Last updated: 2026-05-21T01:53:48.034Z
Last updated: 2026-05-21T06:54:27.698Z
| Контролёр | Состояние | Детали |
|---|---|---|
| C1 L1-watcher | ✅ | [l1-watcher] OK — 0 drift |
| C2 Cross-ref consistency | | [cross-ref-checker] OK — 0 drift in 4 files |
| C2 Cross-ref consistency | 🔴 | Update cross-refs in offending 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) · 16 missed activation(s) — see /brain-retro |
| 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).
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 16. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
## Алерт-индикаторы
File diff suppressed because it is too large Load Diff
+12
View File
@@ -176,6 +176,18 @@ pre-commit:
cross-ref-checker detected version drift in §0 cross-refs.
Update the offending file's cross-ref to match the target's header.
# 12b. extract-node-dormancy — регенерирует tools/.node-dormancy.json
# из Tooling Прил.Н §3.5/§4.X (Pravila §16.4 v1.36, missed-activation
# matcher). Учитывает два сигнала: dormant=true в строке атрибутов или
# ключевое слово DEFERRED в колонке boundaries. Регенерированный JSON
# авто-стейджится — попадает в тот же коммит, что и правки Tooling.
- name: extract-node-dormancy
glob: "docs/Tooling_v8_3.md"
run: node tools/extract-node-dormancy.mjs && git add tools/.node-dormancy.json
fail_text: |
extract-node-dormancy failed.
Проверьте формат 9-attribute table rows в docs/Tooling_v8_3.md.
# 13. observer-of-observer — счётчик чтений docs/observer/ + 54-week self-prune
# (brain governance C3, ADR-011 spec §6.3). Скрипт всегда exit 0 (warn-only by
# design). При observer infrastructure не используется >=54 недель — warn.
+69
View File
@@ -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
}
+17 -3
View File
@@ -7,6 +7,7 @@
* Security Guidance #40: pure parsing no exec/execSync.
*/
import { readFileSync, existsSync } from 'fs';
import { detectMissedActivations } from './missed-activations.mjs';
const SIZE_SMALL = 20;
const SIZE_LARGE = 60;
@@ -192,8 +193,8 @@ export function buildFactorMatrix(episodesWithOutcome) {
return matrix;
}
/** Full deterministic aggregation: dedup → infer outcomes → group → chains → matrix. */
export function analyze(episodes) {
/** Full deterministic aggregation: dedup → infer outcomes → group → chains → matrix → missed activations. */
export function analyze(episodes, options = {}) {
const deduped = dedupeEpisodes(episodes);
const allNormal = deduped.filter((e) => !e.observer_error);
// v1 episodes lack environment / prompt_signal / decision_provenance — they
@@ -205,6 +206,8 @@ export function analyze(episodes) {
episode._inferredOutcome = inferOutcome(episode, eps[i + 1]);
});
}
const classificationMap = options.classificationMap || {};
const dormancy = options.dormancy || {};
return {
episodeCount: normal.length,
v1SkippedCount,
@@ -212,6 +215,7 @@ export function analyze(episodes) {
tasks: groupEpisodesToTasks(normal),
causalChains: findCausalChains(normal),
factorMatrix: buildFactorMatrix(normal),
missedActivations: detectMissedActivations(normal, classificationMap, dormancy),
};
}
@@ -233,7 +237,17 @@ function loadEpisodes(files) {
}
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/brain-retro-analyzer.mjs')) {
const result = analyze(loadEpisodes(process.argv.slice(2)));
const classificationMap = (() => {
try {
return JSON.parse(readFileSync('tools/observer-classification-map.json', 'utf-8')).map || {};
} catch { return {}; }
})();
const dormancy = (() => {
try {
return JSON.parse(readFileSync('tools/.node-dormancy.json', 'utf-8'));
} catch { return {}; }
})();
const result = analyze(loadEpisodes(process.argv.slice(2)), { classificationMap, dormancy });
console.log(JSON.stringify(result, null, 2));
process.exit(0);
}
+26
View File
@@ -263,3 +263,29 @@ describe('inferOutcome — neutral → soft_success (Task 16)', () => {
expect(inferOutcome({ events: [] }, { prompt_signal: 'approval' })).toBe('success');
});
});
describe('analyze() — missedActivations integration', () => {
it('includes missedActivations in the result', () => {
const eps = [
{
schema_version: 2,
task_id: 't1',
timestamps: { started_at: '2026-05-21T00:00:00Z' },
primary_rationale: { node_chosen: 'direct', task_classification: 'refactor' },
events: [],
},
];
const map = { refactor: ['#11'], other: [] };
const dormancy = { '#11': false };
const result = analyze(eps, { classificationMap: map, dormancy });
expect(result.missedActivations).toBeDefined();
expect(result.missedActivations.totalMissed).toBe(1);
expect(result.missedActivations.byNode).toEqual({ '#11': 1 });
});
it('returns missedActivations.totalMissed=0 when no map/dormancy provided', () => {
const eps = [{ schema_version: 2, task_id: 't1', timestamps: { started_at: 'x' }, primary_rationale: { node_chosen: 'direct', task_classification: 'refactor' }, events: [] }];
const result = analyze(eps);
expect(result.missedActivations.totalMissed).toBe(0);
});
});
+39
View File
@@ -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`);
}
+53
View File
@@ -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 | <name> | <kind> | <phase> | <subcat or —> | «<triggers>» | <ADR-NNN or none> | 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 });
});
});
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Missed-activation matcher (Pravila §16.4 v1.36 conditional rule).
* Pure deterministic read-only, no exec, no fs.
*
* An episode is "missed" iff:
* 1. schema_version === 2 (v1 lacks factor data)
* 2. NOT observer_error
* 3. primary_rationale.task_classification map AND map[c].length > 0
* 4. primary_rationale.node_chosen === 'direct' (no explicit node)
* 5. AT LEAST ONE recommended node is non-dormant
*
* Threshold: single episode (per Pravila §16.4 v1.36).
* DEFERRED-узлы filtered via dormancy registry (dormancy[id] === true means
* unavailable covers both Tooling-marked dormant nodes and DEFERRED-in-
* boundaries nodes, normalized by tools/extract-node-dormancy.mjs).
*/
export function detectMissedActivations(episodes, classificationMap, dormancy) {
const byNode = {};
const byClassification = {};
let totalMissed = 0;
for (const e of episodes) {
if (!e || e.observer_error) continue;
if (e.schema_version !== 2) continue;
const pr = e.primary_rationale || {};
const cls = pr.task_classification;
const chosen = pr.node_chosen;
if (!cls || chosen !== 'direct') continue;
const recommended = classificationMap[cls];
if (!Array.isArray(recommended) || recommended.length === 0) continue;
const live = recommended.filter((id) => dormancy[id] === false);
if (live.length === 0) continue;
totalMissed += 1;
byClassification[cls] = (byClassification[cls] || 0) + 1;
for (const id of live) {
byNode[id] = (byNode[id] || 0) + 1;
}
}
return { totalMissed, byNode, byClassification };
}
+78
View File
@@ -0,0 +1,78 @@
// tools/missed-activations.test.mjs
import { describe, it, expect } from 'vitest';
import { detectMissedActivations } from './missed-activations.mjs';
const map = {
refactor: ['#11', '#12', '#43'],
bugfix: ['#18', '#34'],
feature: ['#19'],
other: [],
};
const dormancy = { '#11': false, '#12': false, '#43': false, '#18': false, '#34': false, '#19': false };
function ep(classification, node_chosen) {
return {
schema_version: 2,
primary_rationale: { task_classification: classification, node_chosen },
};
}
describe('detectMissedActivations', () => {
it('counts an episode with profile classification + node_chosen=direct as missed', () => {
const result = detectMissedActivations([ep('refactor', 'direct')], map, dormancy);
expect(result.totalMissed).toBe(1);
expect(result.byNode).toEqual({ '#11': 1, '#12': 1, '#43': 1 });
});
it('does NOT count episode when the recommended node IS chosen', () => {
const result = detectMissedActivations([ep('refactor', '#11')], map, dormancy);
expect(result.totalMissed).toBe(0);
});
it('does NOT count episode when classification=other (empty list)', () => {
const result = detectMissedActivations([ep('other', 'direct')], map, dormancy);
expect(result.totalMissed).toBe(0);
});
it('excludes dormant (DEFERRED) nodes from recommendations', () => {
const dorm = { ...dormancy, '#43': true };
const result = detectMissedActivations([ep('refactor', 'direct')], map, dorm);
expect(result.byNode).toEqual({ '#11': 1, '#12': 1 });
expect(result.totalMissed).toBe(1);
});
it('returns totalMissed=0 when ALL recommended nodes are dormant', () => {
const dorm = { '#11': true, '#12': true, '#43': true };
const result = detectMissedActivations([ep('refactor', 'direct')], map, dorm);
expect(result.totalMissed).toBe(0);
expect(result.byNode).toEqual({});
});
it('ignores schema v1 episodes (no factor analysis)', () => {
const v1 = { schema_version: 1, primary_rationale: { task_classification: 'refactor', node_chosen: 'direct' } };
const result = detectMissedActivations([v1], map, dormancy);
expect(result.totalMissed).toBe(0);
});
it('ignores observer_error markers', () => {
const err = { observer_error: true };
const result = detectMissedActivations([err], map, dormancy);
expect(result.totalMissed).toBe(0);
});
it('ignores unknown classification (not in map)', () => {
const result = detectMissedActivations([ep('unknown-bucket', 'direct')], map, dormancy);
expect(result.totalMissed).toBe(0);
});
it('aggregates byClassification breakdown for the report', () => {
const eps = [
ep('refactor', 'direct'),
ep('refactor', 'direct'),
ep('bugfix', 'direct'),
];
const result = detectMissedActivations(eps, map, dormancy);
expect(result.byClassification).toEqual({ refactor: 2, bugfix: 1 });
expect(result.totalMissed).toBe(3);
});
});
+16
View File
@@ -0,0 +1,16 @@
{
"$schema_version": 1,
"description": "Mapping from observer transcript-parser task_classification values to recommended Tooling Прил.Н node IDs. Source of truth for missed-activation detection (Pravila §16.4 conditional rule). 'other' deliberately empty — no recommendation, never counts as missed. DEFERRED-узлы filtered out by .node-dormancy.json at runtime.",
"map": {
"refactor": ["#11", "#12", "#43", "#64", "#65"],
"bugfix": ["#18", "#34"],
"feature": ["#19"],
"planning": ["#19", "#41", "#42"],
"memory-sync": ["#33"],
"monitoring": ["#34", "#35"],
"analysis": ["#25", "#39", "#53"],
"cleanup": ["#11", "#12"],
"question": ["#60"],
"other": []
}
}
+39 -3
View File
@@ -17,6 +17,8 @@
*/
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { detectMissedActivations } from './missed-activations.mjs';
import { dedupeEpisodes } from './brain-retro-analyzer.mjs';
/**
* @param {number} episodeCount - episodes in the current month JSONL
@@ -59,6 +61,31 @@ function countEpisodes(root) {
return readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean).length;
}
function loadEpisodes(root) {
const month = new Date().toISOString().slice(0, 7);
const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`);
if (!existsSync(file)) return [];
const out = [];
for (const line of readFileSync(file, 'utf-8').split('\n')) {
const t = line.trim();
if (!t) continue;
try { out.push(JSON.parse(t)); } catch { /* skip */ }
}
return out;
}
function loadClassificationMap(root) {
try {
return JSON.parse(readFileSync(join(root, 'tools', 'observer-classification-map.json'), 'utf-8')).map || {};
} catch { return {}; }
}
function loadDormancy(root) {
try {
return JSON.parse(readFileSync(join(root, 'tools', '.node-dormancy.json'), 'utf-8'));
} catch { return {}; }
}
function readSettings(root) {
try {
return JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8'));
@@ -81,14 +108,23 @@ export function runCoverageChecker(root = process.cwd()) {
const hookRegistered = isObserverStopRegistered(settings);
const coverage = checkCoverage(countEpisodes(root), hookRegistered);
const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit')));
return { coverage, registration };
const episodes = loadEpisodes(root).filter((e) => e && e.schema_version === 2 && !e.observer_error);
const missed = detectMissedActivations(
dedupeEpisodes(episodes),
loadClassificationMap(root),
loadDormancy(root)
);
return { coverage, registration, missed };
}
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-coverage-checker.mjs')) {
const { coverage, registration } = runCoverageChecker();
const { coverage, registration, missed } = runCoverageChecker();
if (!coverage.ok) console.warn(`[observer-coverage-checker] WARN — coverage: ${coverage.detail}`);
if (!registration.ok) console.warn(`[observer-coverage-checker] WARN — registration: ${registration.detail}`);
if (coverage.ok && registration.ok) {
if (missed.totalMissed > 0) {
console.warn(`[observer-coverage-checker] WARN — missed activations: ${missed.totalMissed} (see /brain-retro)`);
}
if (coverage.ok && registration.ok && missed.totalMissed === 0) {
console.log(`[observer-coverage-checker] OK — ${coverage.detail}; ${registration.detail}`);
}
process.exit(0); // warn-only — never blocks a commit
+11 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { checkCoverage, checkRegistration } from './observer-coverage-checker.mjs';
import { checkCoverage, checkRegistration, runCoverageChecker } from './observer-coverage-checker.mjs';
describe('checkCoverage', () => {
// COV-1 fix: the metric is driven by Stop-hook registration, NOT by recent
@@ -59,3 +59,13 @@ describe('checkRegistration', () => {
expect(checkRegistration({}, false).ok).toBe(false);
});
});
describe('runCoverageChecker — missed surfacing', () => {
it('returns a missed field with totalMissed', () => {
const { missed } = runCoverageChecker();
expect(missed).toBeDefined();
expect(typeof missed.totalMissed).toBe('number');
expect(missed.byNode).toBeDefined();
expect(missed.byClassification).toBeDefined();
});
});
+10 -6
View File
@@ -11,6 +11,7 @@ function iconFor(status) {
export function renderStatus(inputs) {
const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo } = inputs;
const c6 = inputs.c6 || { status: 'ok', detail: '—' };
const missed = inputs.missed || { totalMissed: 0, byNode: {}, byClassification: {} };
const retroLine = (lastRetroDaysAgo === null || lastRetroDaysAgo === undefined)
? 'never'
: `${lastRetroDaysAgo} day(s) ago`;
@@ -32,7 +33,7 @@ Last updated: ${now}
- Observer evidence: ${observer.episodeCount} episodes this month, ${observer.observerErrors} observer_error markers, ${observer.piiMatches} PII matches before filter
- Legacy v1 episodes (not in factor analysis): ${observer.v1Episodes || 0}
- Last /brain-retro: ${retroLine}
- Использование узлов: см. \`/brain-retro\` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store).
- Использование узлов: см. \`/brain-retro\` (раз в спринт). missed_activations: ${missed.totalMissed}. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store).
## Алерт-индикаторы
@@ -106,16 +107,18 @@ function countV1Episodes() {
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md-generator.mjs')) {
const cov = runCoverageChecker();
const c5ok = cov.coverage.ok && cov.registration.ok;
const c5ok = cov.coverage.ok && cov.registration.ok && cov.missed.totalMissed === 0;
const c5detail = [
cov.coverage.detail,
cov.registration.detail,
cov.missed.totalMissed > 0 ? `${cov.missed.totalMissed} missed activation(s) — see /brain-retro` : null,
].filter(Boolean).join(' · ');
const inputs = {
now: new Date().toISOString(),
c1: runControllerNode(['tools/l1-watcher.mjs']),
c2: runControllerNode(['tools/cross-ref-checker.mjs']),
c3: runControllerNode(['tools/observer-of-observer.mjs', 'check']),
c5: {
status: c5ok ? 'ok' : 'warn',
detail: [cov.coverage.detail, cov.registration.detail].join(' · '),
},
c5: { status: c5ok ? 'ok' : 'warn', detail: c5detail },
c6: runControllerNode(['tools/observer-chain-map-checker.mjs']),
observer: {
episodeCount: countEpisodes(),
@@ -123,6 +126,7 @@ if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md-
piiMatches: countPiiMatches(),
v1Episodes: countV1Episodes(),
},
missed: cov.missed,
lastRetroDaysAgo: lastRetroDaysAgo(),
};
const md = renderStatus(inputs);
+30 -2
View File
@@ -9,6 +9,7 @@ const baseInputs = (overrides = {}) => ({
c5: { status: 'ok', detail: 'coverage OK · registration OK' },
c6: { status: 'ok', detail: '14 chains in sync' },
observer: { episodeCount: 12, observerErrors: 0, piiMatches: 0 },
missed: { totalMissed: 0, byNode: {}, byClassification: {} },
...overrides,
});
@@ -44,9 +45,10 @@ describe('renderStatus', () => {
expect(md).toContain('| C1 L1-watcher | 🔴');
});
it('mentions the capability-readiness behavioral rule', () => {
it('mentions the conditional capability-readiness behavioral rule (§16.4 v1.36)', () => {
const md = renderStatus(baseInputs());
expect(md).toContain('capability-readiness');
expect(md).toContain('Неиспользованные узлы — не алерт');
expect(md).toContain('если профильной задачи не было');
expect(md).toContain('feedback_brain_unused_tools_not_problem');
});
@@ -81,3 +83,29 @@ describe('renderStatus — v1 episodes count surface (Task 18)', () => {
expect(md).toMatch(/Legacy v1 episodes \(not in factor analysis\):\s*0/);
});
});
describe('renderStatus — missed activations (Task 7, Pravila §16.4 v1.36)', () => {
it('renders missed_activations: 0 when there are no misses', () => {
const md = renderStatus(baseInputs());
expect(md).toContain('missed_activations: 0');
});
it('renders missed_activations: N when misses occur', () => {
const md = renderStatus(baseInputs({
missed: { totalMissed: 3, byNode: { '#11': 2, '#12': 1 }, byClassification: { refactor: 3 } },
}));
expect(md).toContain('missed_activations: 3');
});
it('keeps C5 ✅ when controller is ok and no misses', () => {
const md = renderStatus(baseInputs());
expect(md).toContain('| C5 Observer-coverage | ✅');
});
it('honors the c5 status override (warn) regardless of missed count', () => {
const md = renderStatus(baseInputs({
c5: { status: 'warn', detail: '16 missed activation(s)' },
}));
expect(md).toContain('| C5 Observer-coverage | ⚠️');
});
});