f6a1b3d09f
Auto-generated блок с разбивкой % дисциплины по типам задач, router-step distribution + suspicious-флаг, boundaries-applied rate. Backward-compat: блок опускается, если discipline не передан. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
206 lines
8.6 KiB
JavaScript
206 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { execFileSync } from 'child_process';
|
|
import { runCoverageChecker } from './observer-coverage-checker.mjs';
|
|
import { analyze } from './brain-retro-analyzer.mjs';
|
|
import { loadRegistry } from './registry-load.mjs';
|
|
import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs';
|
|
|
|
function iconFor(status) {
|
|
return { ok: '✅', warn: '⚠️', fail: '🔴' }[status] || '⚪';
|
|
}
|
|
|
|
export function renderStatus(inputs) {
|
|
const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo, discipline } = inputs;
|
|
const c6 = inputs.c6 || { status: 'ok', detail: '—' };
|
|
const missed = inputs.missed || { totalMissed: 0, byNode: {}, byClassification: {} };
|
|
|
|
function formatPercent(p) { return `${(p * 100).toFixed(1)}%`; }
|
|
|
|
let disciplineBlock = '';
|
|
if (discipline) {
|
|
const rows = Object.entries(discipline.byClassification || {})
|
|
.sort((a, b) => b[1].episodes - a[1].episodes)
|
|
.map(([cls, b]) => `| ${cls} | ${b.episodes} | ${formatPercent(b.pctTriggerMatch)} | ${formatPercent(b.pctViaSkill)} |`)
|
|
.join('\n');
|
|
const stepDist = Object.entries(discipline.routerStep?.distribution || {})
|
|
.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
const suspicious = discipline.routerStep?.suspicious
|
|
? ' ⚠️ suspicious — >90% эпизодов остановились на step=1 (вероятный sentinel-bug парсера)'
|
|
: '';
|
|
const boundariesPct = formatPercent(discipline.boundariesRate?.rate || 0);
|
|
disciplineBlock = `
|
|
## Метрики дисциплины
|
|
|
|
Baseline дисциплины роутера (этап 2 router discipline overhaul, spec 2026-05-23). Цель — увидеть «точку До» перед enforcement-хуком этапа 3.
|
|
|
|
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|
|
|---|---|---|---|
|
|
${rows || '| (no data) | 0 | 0% | 0% |'}
|
|
|
|
Router step distribution: ${stepDist || '(empty)'}${suspicious}
|
|
|
|
Boundaries applied (ADR / границы): ${discipline.boundariesRate?.withBoundaries || 0} of ${discipline.boundariesRate?.total || 0} эпизодов (${boundariesPct}).
|
|
`;
|
|
}
|
|
|
|
const activeProjects = (inputs.activeProjects || '').trim();
|
|
const projectsBlock = activeProjects
|
|
? `\n## Активные многоэтапные проекты\n\n${activeProjects}\n`
|
|
: '';
|
|
const retroLine = (lastRetroDaysAgo === null || lastRetroDaysAgo === undefined)
|
|
? 'never'
|
|
: `${lastRetroDaysAgo} day(s) ago`;
|
|
return `# Brain Status (auto-generated)
|
|
|
|
Last updated: ${now}
|
|
|
|
| Контролёр | Состояние | Детали |
|
|
|---|---|---|
|
|
| C1 L1-watcher | ${iconFor(c1.status)} | ${c1.detail || '—'} |
|
|
| C2 Cross-ref consistency | ${iconFor(c2.status)} | ${c2.detail || '—'} |
|
|
| C3 Observer-of-observer | ${iconFor(c3.status)} | ${c3.detail || '—'} |
|
|
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
|
| C5 Observer-coverage | ${iconFor(c5.status)} | ${c5.detail || '—'} |
|
|
| C6 Chain map sync | ${iconFor(c6.status)} | ${c6.detail || '—'} |
|
|
|
|
## Метрики (информационные, не алерты)
|
|
|
|
- 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\` (раз в спринт). missed_activations: ${missed.totalMissed}. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store).
|
|
${disciplineBlock}${projectsBlock}
|
|
## Алерт-индикаторы
|
|
|
|
✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось
|
|
`;
|
|
}
|
|
|
|
function runControllerNode(scriptArgs) {
|
|
try {
|
|
const out = execFileSync('node', scriptArgs, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
return { status: 'ok', detail: out.trim().split('\n').pop() };
|
|
} catch (err) {
|
|
return { status: 'fail', detail: (err.stderr || err.message || '').trim().split('\n').pop() };
|
|
}
|
|
}
|
|
|
|
function countEpisodes() {
|
|
const dir = 'docs/observer';
|
|
if (!existsSync(dir)) return 0;
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const file = join(dir, `episodes-${month}.jsonl`);
|
|
if (!existsSync(file)) return 0;
|
|
return readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean).length;
|
|
}
|
|
|
|
function countPiiMatches() {
|
|
const file = join('docs', 'observer', '.pii-counters.json');
|
|
if (!existsSync(file)) return 0;
|
|
try {
|
|
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const monthCounts = data[month] || {};
|
|
return Object.values(monthCounts).reduce((s, n) => s + n, 0);
|
|
} catch { return 0; }
|
|
}
|
|
|
|
function lastRetroDaysAgo() {
|
|
const file = join('docs', 'observer', '.read-counter.json');
|
|
if (!existsSync(file)) return null;
|
|
try {
|
|
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
if (!data.last_read_at) return null;
|
|
return Math.floor((Date.now() - new Date(data.last_read_at).getTime()) / 86400000);
|
|
} catch { return null; }
|
|
}
|
|
|
|
function countObserverErrors() {
|
|
const dir = 'docs/observer';
|
|
if (!existsSync(dir)) return 0;
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const file = join(dir, `episodes-${month}.jsonl`);
|
|
if (!existsSync(file)) return 0;
|
|
return readFileSync(file, 'utf-8')
|
|
.trim()
|
|
.split('\n')
|
|
.filter((l) => l.includes('"observer_error":true')).length;
|
|
}
|
|
|
|
/** Legacy v1 episode count — lines without schema_version=2, not observer_error markers. */
|
|
function countV1Episodes() {
|
|
const dir = 'docs/observer';
|
|
if (!existsSync(dir)) return 0;
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const file = join(dir, `episodes-${month}.jsonl`);
|
|
if (!existsSync(file)) return 0;
|
|
return readFileSync(file, 'utf-8')
|
|
.trim()
|
|
.split('\n')
|
|
.filter((l) => l && !l.includes('"schema_version":2') && !l.includes('"observer_error":true')).length;
|
|
}
|
|
|
|
function loadCurrentMonthEpisodes() {
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const file = join('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;
|
|
}
|
|
|
|
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 && 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: c5detail },
|
|
c6: runControllerNode(['tools/observer-chain-map-checker.mjs']),
|
|
observer: {
|
|
episodeCount: countEpisodes(),
|
|
observerErrors: countObserverErrors(),
|
|
piiMatches: countPiiMatches(),
|
|
v1Episodes: countV1Episodes(),
|
|
},
|
|
missed: cov.missed,
|
|
lastRetroDaysAgo: lastRetroDaysAgo(),
|
|
activeProjects: existsSync('docs/observer/active-projects.md')
|
|
? readFileSync('docs/observer/active-projects.md', 'utf-8')
|
|
: '',
|
|
discipline: (() => {
|
|
try {
|
|
const registry = loadRegistry({ useCache: false });
|
|
const classificationMap = buildClassificationMap(registry);
|
|
const dormancy = buildDormancyMap(registry);
|
|
const eps = loadCurrentMonthEpisodes();
|
|
const a = analyze(eps, { classificationMap, dormancy });
|
|
return {
|
|
byClassification: a.disciplineByClassification,
|
|
routerStep: a.routerStep,
|
|
boundariesRate: a.boundariesRate,
|
|
};
|
|
} catch (err) {
|
|
console.warn('[status-md-generator] discipline calc skipped:', err.message);
|
|
return null;
|
|
}
|
|
})(),
|
|
};
|
|
const md = renderStatus(inputs);
|
|
writeFileSync('docs/observer/STATUS.md', md);
|
|
console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`);
|
|
}
|