CRSS Exam-Prep Pipeline
Corpus checks, snapshots, edit digests and SME queues for an exam bank.
PipelinesLivePythonNode.jsSupabase
0builds
What it is
A deterministic content pipeline for the CRSS recovery-support question bank: gated corpus checks, bank snapshots, edit digests and an SME review queue, with every build recorded as a pipeline run.
Take it with you
The real, committed source behind this system — copy it or download the file. Plus a portable spec of everything on this page.
corpus-checks-lib.mjsjavascript186 lines
The corpus-checks library — the gate of pure checks the exam bank must pass before any snapshot or digest is produced.
// ---------------------------------------------------------------------------
// Pure-logic corpus-level QA checks (Layer 5, deterministic subset).
//
// Each check is a function that takes an array of normalized items and a
// config object, and returns a "check result" of the shape expected by
// the crss_corpus_checks table:
//
// {
// check_code: 'answer_distribution',
// layer: 'L5',
// status: 'pass' | 'warn' | 'fail',
// observed: {...},
// expected: {...},
// affected_items: [id, ...], // optional
// notes: '...' // optional
// }
//
// No I/O, no env access — makes the logic trivial to unit-test.
// ---------------------------------------------------------------------------
export const ANSWER_LETTERS = ['A', 'B', 'C', 'D'];
// Layer 5a: answer-letter distribution ~25% each.
export function answerDistributionCheck(items, opts = {}) {
const min = opts.minPct ?? 0.2;
const max = opts.maxPct ?? 0.3;
const counts = { A: 0, B: 0, C: 0, D: 0 };
for (const it of items) {
if (counts[it.correct] !== undefined) counts[it.correct] += 1;
}
const total = items.length;
const pcts = {};
for (const l of ANSWER_LETTERS) pcts[l] = total ? counts[l] / total : 0;
const outOfBand = ANSWER_LETTERS.filter((l) => pcts[l] < min || pcts[l] > max);
const status = total === 0 ? 'fail' : outOfBand.length === 0 ? 'pass' : 'warn';
return {
check_code: 'answer_distribution',
layer: 'L5',
status,
observed: { counts, pcts, total },
expected: { min, max, target: 0.25 },
affected_items: [],
notes: outOfBand.length
? `Letters outside ${(min * 100).toFixed(0)}%–${(max * 100).toFixed(0)}% band: ${outOfBand.join(', ')}`
: null,
};
}
// Layer 5b: blueprint weights — domain proportions vs. target.
// `targets` is an object like {domain: 0.20, ...} summing to ~1.
export function blueprintWeightsCheck(items, { targets, toleranceBp = 0.03 } = {}) {
const total = items.length;
const counts = {};
for (const it of items) counts[it.domain] = (counts[it.domain] ?? 0) + 1;
const observed = {};
const drift = {};
const breaches = [];
for (const domain of Object.keys(targets ?? {})) {
const pct = total ? (counts[domain] ?? 0) / total : 0;
observed[domain] = { count: counts[domain] ?? 0, pct };
const d = pct - targets[domain];
drift[domain] = d;
if (Math.abs(d) > toleranceBp) breaches.push(domain);
}
for (const domain of Object.keys(counts)) {
if (targets && !(domain in targets)) {
observed[domain] = { count: counts[domain], pct: counts[domain] / total, untargeted: true };
}
}
const status = total === 0
? 'fail'
: breaches.length === 0
? 'pass'
: breaches.length <= 2
? 'warn'
: 'fail';
return {
check_code: 'blueprint_weights',
layer: 'L5',
status,
observed: { counts: observed, drift, total },
expected: { targets, tolerance_bp: toleranceBp },
affected_items: [],
notes: breaches.length ? `Drift > ${(toleranceBp * 100).toFixed(1)}pp in: ${breaches.join(', ')}` : null,
};
}
// Layer 5c: module coverage. Study guide has 48 modules; any item whose
// subcategory falls outside that set is an orphan. Any module with fewer
// than `minPerModule` items is under-covered.
export function moduleCoverageCheck(items, { allowedModules, minPerModule = 3 } = {}) {
const counts = new Map();
for (const it of items) {
const m = it.subcategory ?? '(none)';
counts.set(m, (counts.get(m) ?? 0) + 1);
}
const allowed = allowedModules instanceof Set
? allowedModules
: Array.isArray(allowedModules)
? new Set(allowedModules)
: null;
const orphans = [];
const underCovered = [];
for (const [module, n] of counts.entries()) {
if (allowed && !allowed.has(module)) orphans.push(module);
else if (n < minPerModule) underCovered.push({ module, count: n });
}
const orphanItems = allowed
? items.filter((it) => !allowed.has(it.subcategory ?? '(none)')).map((it) => it.id)
: [];
const status = orphans.length > 0
? 'fail'
: underCovered.length > 0
? 'warn'
: 'pass';
return {
check_code: 'module_coverage',
layer: 'L5',
status,
observed: {
unique_modules: counts.size,
per_module: Object.fromEntries(counts),
orphan_modules: orphans,
under_covered: underCovered,
},
expected: {
allowed_modules: allowed ? [...allowed] : null,
min_per_module: minPerModule,
},
affected_items: orphanItems,
notes: orphans.length
? `Orphan modules (not in allowed list): ${orphans.join(', ')}`
: underCovered.length
? `Modules below ${minPerModule} items: ${underCovered.map((x) => x.module).join(', ')}`
: null,
};
}
// Layer 5d: cog-level distribution. Warn on extreme skew.
const COG_LEVELS = ['recall', 'understanding', 'application', 'analysis'];
export function cogLevelDistributionCheck(items, { minPctAnalysis = 0.05 } = {}) {
const counts = Object.fromEntries(COG_LEVELS.map((l) => [l, 0]));
let untagged = 0;
for (const it of items) {
if (COG_LEVELS.includes(it.cog_level)) counts[it.cog_level] += 1;
else untagged += 1;
}
const total = items.length;
const pcts = Object.fromEntries(
COG_LEVELS.map((l) => [l, total ? counts[l] / total : 0])
);
const analysisLow = pcts.analysis < minPctAnalysis;
const status = total === 0 ? 'fail' : analysisLow ? 'warn' : 'pass';
return {
check_code: 'cog_level_distribution',
layer: 'L5',
status,
observed: { counts, pcts, untagged, total },
expected: { min_pct_analysis: minPctAnalysis },
affected_items: [],
notes: analysisLow
? `analysis items are ${(pcts.analysis * 100).toFixed(1)}% of bank (floor ${(minPctAnalysis * 100).toFixed(0)}%)`
: null,
};
}
export function runAllChecks(items, config = {}) {
return [
answerDistributionCheck(items, config.answer_distribution),
blueprintWeightsCheck(items, config.blueprint_weights),
moduleCoverageCheck(items, config.module_coverage),
cogLevelDistributionCheck(items, config.cog_level_distribution),
];
}
Where it lives
- scripts/crss/
- crss-exam-prep/
- src/app/crss/
See it in action
CRSS →FAQ
What guards the CRSS bank?
A set of corpus checks must pass before any snapshot or digest is produced — the gate that keeps the bank consistent.
Related systems
Want a system like this built for you?Work with me →