#!/usr/bin/env node
/**
* Gate Index Generator
*
* Reads all gate.yaml files and generates gates/_index.md.
* Source of truth is the YAML files — the index is derived, never manually edited.
*
* Usage:
* node scripts/generate-gate-index.js [--check]
*
* Options:
* --check Verify index is up-to-date without writing (exit 1 if stale)
*/
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
const __dirname = dirname(fileURLToPath(import.meta.url));
const GATES_DIR = join(__dirname, '..', 'resources', 'gates');
const INDEX_PATH = join(GATES_DIR, '_index.md');
const CHECK_MODE = process.argv.includes('--check');
// ============================================
// DISCOVERY
// ============================================
function discoverGates() {
return readdirSync(GATES_DIR, { withFileTypes: true })
.filter((e) => e.isDirectory() && e.name !== 'config')
.map((e) => {
const yamlPath = join(GATES_DIR, e.name, 'gate.yaml');
if (!existsSync(yamlPath)) return null;
try {
const data = yaml.load(readFileSync(yamlPath, 'utf-8'));
return { dir: e.name, ...data };
} catch (err) {
console.warn(` ⚠ Failed to parse ${e.name}/gate.yaml: ${err.message}`);
return null;
}
})
.filter(Boolean);
}
// ============================================
// CLASSIFICATION (inferred from existing fields)
// ============================================
function classifyGate(gate) {
const cats = gate.activation?.prompt_categories ?? [];
const hasFramework = gate.activation?.framework_context?.length > 0;
// PR review gates
if (cats.includes('pr-review')) return 'PR Review';
// Framework methodology
if (hasFramework || gate.gate_type === 'framework') return 'Framework';
// Planning / workflow
if (cats.includes('planning') && !cats.includes('code')) return 'Planning';
// Testing
if (gate.id.startsWith('test-')) return 'Testing';
// Security
if (gate.id.includes('security')) return 'Security';
// Research / analysis
if (cats.includes('research') && !cats.includes('development')) return 'Research';
// Development (broad)
return 'Development';
}
function severityBadge(gate) {
const s = gate.severity ?? '—';
const e = gate.enforcementMode ?? null;
if (e === 'advisory') return `${s} (advisory)`;
if (e === 'blocking') return `${s} (blocking)`;
return s;
}
function activationSummary(gate) {
const parts = [];
const cats = gate.activation?.prompt_categories ?? [];
const explicit = gate.activation?.explicit_request;
const frameworks = gate.activation?.framework_context ?? [];
if (cats.length > 0) parts.push(cats.join(', '));
if (frameworks.length > 0) parts.push(`frameworks: ${frameworks.join(', ')}`);
if (explicit) parts.push('explicit only');
if (parts.length === 0) return 'always';
return parts.join(' · ');
}
// ============================================
// RENDER
// ============================================
function renderIndex(gates) {
const grouped = {};
for (const gate of gates) {
const group = classifyGate(gate);
if (!grouped[group]) grouped[group] = [];
grouped[group].push(gate);
}
// Stable group order
const groupOrder = [
'Development',
'Security',
'Testing',
'Planning',
'Research',
'PR Review',
'Framework',
];
const lines = [
'<!-- Generated by scripts/generate-gate-index.js — do not edit manually -->',
'',
'# Gate Index',
'',
`${gates.length} gates across ${Object.keys(grouped).length} groups.`,
'',
];
for (const group of groupOrder) {
const items = grouped[group];
if (!items?.length) continue;
// Sort within group: severity critical > high > medium > none, then alphabetical
const severityRank = { critical: 0, high: 1, medium: 2 };
items.sort((a, b) => {
const ra = severityRank[a.severity] ?? 3;
const rb = severityRank[b.severity] ?? 3;
if (ra !== rb) return ra - rb;
return a.id.localeCompare(b.id);
});
lines.push(`## ${group}`, '');
lines.push('| Gate | Severity | Activation | Description |');
lines.push('|------|----------|------------|-------------|');
for (const gate of items) {
const desc = (gate.description ?? '').replace(/\n/g, ' ').trim();
lines.push(
`| \`${gate.id}\` | ${severityBadge(gate)} | ${activationSummary(gate)} | ${desc} |`
);
}
lines.push('');
}
lines.push('---', '');
lines.push(`*Generated: ${new Date().toISOString().split('T')[0]}*`, '');
return lines.join('\n');
}
// ============================================
// MAIN
// ============================================
function main() {
const gates = discoverGates();
if (gates.length === 0) {
console.log('No gates found.');
process.exit(0);
}
const content = renderIndex(gates);
if (CHECK_MODE) {
const existing = existsSync(INDEX_PATH) ? readFileSync(INDEX_PATH, 'utf-8') : '';
// Compare ignoring the Generated date line
const normalize = (s) => s.replace(/\*Generated:.*\*/, '').trim();
if (normalize(existing) !== normalize(content)) {
console.error('✗ Gate index is stale. Run: node scripts/generate-gate-index.js');
process.exit(1);
}
console.log('✓ Gate index is up-to-date');
process.exit(0);
}
writeFileSync(INDEX_PATH, content, 'utf-8');
console.log(`✓ Generated ${INDEX_PATH}`);
console.log(` ${gates.length} gates indexed`);
}
main();