b917360e9b
Phase 1 Task 4 of LLM-first router overhaul. Aggressive scope per user choice (AskUserQuestion 2026-05-25). Pravila changes: - §12 (lines 678-748) extracted to docs/archive/.../pravila-12/, body replaced by 1-paragraph placeholder pointing to §17 (Task 5) + ADR-016. - §0 priority chain dropped §12, added forward note about §17. - §16.4 cross-refs migrated: tools/observer-classification-map.json -> docs/registry/nodes.yaml + buildClassificationMap; tools/.node-dormancy.json -> nodes.yaml status field + buildDormancyMap. - §16.5 hard-rule list: §12 -> §17. Code refactor (preserves test green): - tools/observer-coverage-checker.mjs + observer-transcript-parser.mjs switched from readFileSync(.json) to loadRegistry + adapter. - 9/9 + 154/154 GREEN. git mv into archive/routing-docs/: - tools/observer-classification-map.json, .node-dormancy.json, extract-node-dormancy.mjs, extract-node-dormancy.test.mjs. lefthook.yml: job 12b removed. Memory (user-level, cp+add-f): - feedback_superpowers_hard_rule.md, feedback_feature_via_writing_plans.md copied to archive/memory/. MEMORY.md user-level updated. Plan deviations (TASKLOG.md): - registry-to-classification-map.mjs KEEP (4+ active consumers). - routing-off-phase.md NOT ARCHIVED (auto-generated derivative). - router-procedure.md deferred. Verification: vitest tools/ 539 passed (baseline 543 -7 dormancy +3 rollback). Rollback: node tools/test-rollback.mjs --execute + git reset --hard brain-pre-llm-bootstrap. Plan: docs/superpowers/plans/2026-05-25-llm-first-router-overhaul.md Task 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
5.5 KiB
JavaScript
144 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* C5 observer-coverage-checker (brain governance, observer factor-analysis
|
|
* spec §5.2). Warn-only — always exits 0. Two checks:
|
|
* 1. Coverage — Stop-hook is registered but 0 episodes this month.
|
|
* Comparing episodes against commit volume is wrong-unit (commits =
|
|
* work-unit, episodes = turn-unit) and wrong-window (the C5 window
|
|
* can predate the hook's registration); a freshly-registered hook
|
|
* vs. 1000 historical commits would flap forever. Driven by hook
|
|
* registration instead — the only honest expectation source.
|
|
* 2. Registration integrity — observer Stop-hook present in
|
|
* .claude/settings.json and .git/hooks/post-commit installed.
|
|
* Findings are surfaced in docs/observer/STATUS.md (C4 generator); this
|
|
* controller never blocks a commit.
|
|
*
|
|
* Security Guidance #40: pure fs — no exec/execSync.
|
|
*/
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { detectMissedActivations } from './missed-activations.mjs';
|
|
import { dedupeEpisodes } from './brain-retro-analyzer.mjs';
|
|
import { loadRegistry } from './registry-load.mjs';
|
|
import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs';
|
|
|
|
/**
|
|
* @param {number} episodeCount - episodes in the current month JSONL
|
|
* @param {boolean} hookRegistered - whether observer-stop-hook is wired in settings
|
|
* @returns {{ok: boolean, detail: string}}
|
|
*/
|
|
export function checkCoverage(episodeCount, hookRegistered) {
|
|
if (hookRegistered && episodeCount === 0) {
|
|
return {
|
|
ok: false,
|
|
detail: `Stop-hook registered but 0 episode(s) recorded this month — hook may be silently failing`,
|
|
};
|
|
}
|
|
return { ok: true, detail: `${episodeCount} episode(s) this month` };
|
|
}
|
|
|
|
/** @returns {{ok: boolean, detail: string}} */
|
|
export function checkRegistration(settingsJson, postCommitExists) {
|
|
const problems = [];
|
|
const stopHooks = (((settingsJson || {}).hooks || {}).Stop) || [];
|
|
const hasObserverStop = stopHooks.some((entry) =>
|
|
((entry && entry.hooks) || []).some((h) => String((h && h.command) || '').includes('observer-stop-hook'))
|
|
);
|
|
if (!hasObserverStop) {
|
|
problems.push('observer-stop-hook NOT registered in .claude/settings.json Stop hook');
|
|
}
|
|
if (!postCommitExists) {
|
|
problems.push('.git/hooks/post-commit not installed (run: npx lefthook install --force)');
|
|
}
|
|
return {
|
|
ok: problems.length === 0,
|
|
detail: problems.length ? problems.join('; ') : 'Stop-hook + post-commit OK',
|
|
};
|
|
}
|
|
|
|
function countEpisodes(root) {
|
|
const month = new Date().toISOString().slice(0, 7);
|
|
const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`);
|
|
if (!existsSync(file)) return 0;
|
|
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 {
|
|
const registry = loadRegistry({
|
|
registryPath: join(root, 'docs', 'registry', 'nodes.yaml'),
|
|
schemaPath: join(root, 'docs', 'registry', 'schema.json'),
|
|
useCache: false,
|
|
});
|
|
return buildClassificationMap(registry);
|
|
} catch { return {}; }
|
|
}
|
|
|
|
function loadDormancy(root) {
|
|
try {
|
|
const registry = loadRegistry({
|
|
registryPath: join(root, 'docs', 'registry', 'nodes.yaml'),
|
|
schemaPath: join(root, 'docs', 'registry', 'schema.json'),
|
|
useCache: false,
|
|
});
|
|
return buildDormancyMap(registry);
|
|
} catch { return {}; }
|
|
}
|
|
|
|
function readSettings(root) {
|
|
try {
|
|
return JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8'));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function isObserverStopRegistered(settings) {
|
|
const stopHooks = (((settings || {}).hooks || {}).Stop) || [];
|
|
return stopHooks.some((entry) =>
|
|
((entry && entry.hooks) || []).some((h) =>
|
|
String((h && h.command) || '').includes('observer-stop-hook')
|
|
)
|
|
);
|
|
}
|
|
|
|
export function runCoverageChecker(root = process.cwd()) {
|
|
const settings = readSettings(root);
|
|
const hookRegistered = isObserverStopRegistered(settings);
|
|
const coverage = checkCoverage(countEpisodes(root), hookRegistered);
|
|
const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit')));
|
|
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, 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 (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
|
|
}
|