Files
portal/tools/observer-coverage-checker.mjs
T
2026-05-19 10:38:25 +03:00

91 lines
3.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 — recent git commits but 0 observer episodes this month.
* 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: git is invoked via execFileSync (argument array,
* no shell) — no exec/execSync.
*/
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { execFileSync } from 'child_process';
const RECENT_WINDOW = '14 days ago';
/** @returns {{ok: boolean, detail: string}} */
export function checkCoverage(episodeCount, recentCommitCount) {
if (recentCommitCount > 0 && episodeCount === 0) {
return {
ok: false,
detail: `${recentCommitCount} commit(s) in the last 2 weeks but 0 observer episodes this month`,
};
}
return { ok: true, detail: `${episodeCount} episode(s), ${recentCommitCount} recent commit(s)` };
}
/** @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 countRecentCommits(root) {
try {
const out = execFileSync('git', ['log', `--since=${RECENT_WINDOW}`, '--oneline'], {
cwd: root,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return out.trim() ? out.trim().split('\n').length : 0;
} catch {
return 0;
}
}
export function runCoverageChecker(root = process.cwd()) {
const coverage = checkCoverage(countEpisodes(root), countRecentCommits(root));
let settings = {};
try {
settings = JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8'));
} catch {
settings = {};
}
const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit')));
return { coverage, registration };
}
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-coverage-checker.mjs')) {
const { coverage, registration } = 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) {
console.log(`[observer-coverage-checker] OK — ${coverage.detail}; ${registration.detail}`);
}
process.exit(0); // warn-only — never blocks a commit
}