Skill Wiki v0.1.0

Docs / implementation / checkers

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:

  1. Cycles in requires — unresolvable load order.
  2. Contradictions — atoms with contradicts edges that also appear in the same composition contract.
  3. Kind mismatchesvalidates-with from 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