On this page
Checkers · L1 / L2 / L3
Three checking layers, each catching a different class of bug. L1 + L3 are pure and mandatory. L2 is opt-in and uses a small LLM.
L1 · Schema (mandatory)
Validates every atom's structure against its kind's schema. Required fields
present? Decorator constraints satisfied? Atom IDs match the
@scope/kind-kebab-name shape? Cross-atom refs resolvable inside the
package?
// packages/compiler/src/checker-l1.ts
export function checkL1(ast: PrimeAST): Diagnostic[] {
const errors: Diagnostic[] = [];
for (const atom of ast.atoms) {
errors.push(...validateRequiredFields(atom));
errors.push(...validateIdShape(atom));
errors.push(...validateKindSchema(atom));
errors.push(...validateEdgeShape(atom));
}
return errors;
} Cost: ~5 ms per atom. Runs even without API keys.
L2 · Semantic LLM check (opt-in)
Calls a small LLM (DeepSeek by default) to catch semantic drift. Asks: does the
body contradict the metadata? A rule whose severity: low but whose
remediation describes a critical accessibility blocker would be flagged here.
// packages/compiler/src/checker-l2.ts (excerpt)
const prompt = `Atom ${atom.id} of kind ${atom.kind}.
Metadata says: severity=${atom.severity}, applies-to=${atom.appliesTo}.
Body says: "${atom.body}"
Does the body's tone, scope, or severity match the metadata?
Reply: { "ok": bool, "reason"?: string }
`;
const verdict = await aiClient.json(prompt);
if (!verdict.ok) {
diagnostics.push({ atom: atom.id, message: verdict.reason });
}
Cost: ~$0.0001/atom on DeepSeek; results are cached by content-hash in
.l2-cache.json. A no-API-key build skips L2 with a notice.
L3 · Cross-atom (mandatory)
Walks the resolved edge graph. Three classes of error:
- Cycles in
requires— unresolvable load order. - Contradictions — atoms with
contradictsedges that also appear in the same composition contract. - Kind mismatches —
validates-withfrom a rule to another rule (must be source/metric/check).
// packages/compiler/src/checker-l3-cross.ts (excerpt)
function detectCycles(graph: EdgeGraph): Cycle[] {
// Tarjan's SCC over the requires-only subgraph
const sccs = stronglyConnectedComponents(graph.subgraph('requires'));
return sccs.filter((scc) => scc.size > 1).map(toCycle);
}
function checkContradicts(atoms: Atom[]): Diagnostic[] {
const ds: Diagnostic[] = [];
for (const atom of atoms) {
for (const c of atom.contradicts) {
const target = byId.get(c);
if (!target) continue;
const sharedScope = scopesContaining(atom).filter(
(s) => scopesContaining(target).includes(s),
);
if (sharedScope.length > 0) {
ds.push(scopeContradictsError(atom, target, sharedScope));
}
}
}
return ds;
} Diagnostic format
All three checkers emit a uniform Diagnostic shape:
interface Diagnostic {
level: 'error' | 'warning' | 'info';
layer: 'L1' | 'L2' | 'L3';
atom: string; // atom id
message: string;
hint?: string; // suggested fix
source?: { file: string; line: number };
} Running checks
# All three on a whole sources tree
prime check --registry
# Only L1 + L3 (no API key needed)
prime check --registry --no-l2
# Single file
prime check primes/sources/@my/rule-x.prime