d080198220
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.5 KiB
JavaScript
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
|
|
}
|