Convergence Model
How convergence checking is integrated into the codegen pipeline — what happens when a node fails, how retries work, and how the system reaches a verified state.
Convergence Model
Convergence is the mechanism that tells Praetor when generated code faithfully implements its specification. A project is converged when every spec node has a corresponding, verified implementation and no generated code exists outside the spec. Until convergence is reached the pipeline retries, escalates, or marks nodes as failed.
For the full CEGIS algorithm — the mathematical framework behind convergence — see Convergence (CEGIS deep-dive).
How Convergence Fits into Codegen
Convergence checking runs after every Kit execution, not at the end of a run. This means the pipeline detects failures at the earliest possible point and feeds precise feedback back into the next attempt.
Kit executes for node N
│
▼
ConvergenceChecker.checkConvergence(projectId, tenantId)
│
├── score >= 95 → status: "converged" → write generated_from edge, continue
├── score 70-94 → status: "partially_converged" → retry with StructuralDiff
├── score 30-69 → status: "diverged" → retry with StructuralDiff
└── score < 30 → status: "not_started" → restart Kit from scratchThe score is a weighted composite across five named checks:
| Check | Weight | Passes When |
|---|---|---|
entityCoverage | 30% | ≥ 80% of spec entity nodes have implementations |
endpointCoverage | 25% | ≥ 80% of spec_endpoint nodes are implemented |
guardCoverage | 20% | Unguarded endpoint count produces score ≥ 80 |
operationCoverage | 15% | ≥ 80% of spec_operation nodes are implemented |
driftCheck | 10% | Fewer than ~20 nodes marked needs_revalidation |
Stack-specific readiness requirements — registered by Kits via registerStackReadinessRequirement() — contribute additional checks. Each registered requirement adds one pass/fail item to the combined score:
finalScore = (genericWeightedScore + stackPassingCount × 100) / (1 + stackTotal)Per-Emitter Routing
Convergence is checked per emitter target. A project's emitter_targets column determines which convergence path runs:
// convergence-router.ts
for (const target of targets) {
if (target === 'config') {
results.push(await checkConfigConvergence(projectId));
} else if (target === 'codegen') {
results.push(await checkCodegenConvergence(projectId, tenantId));
}
}ProjectConvergenceResult.overallDone is only true when every emitter target reports isDone: true. This prevents a project from appearing converged when only one of its targets (e.g., config) has been satisfied.
What Happens When a Node Fails
When a node's convergence check fails, the pipeline computes a StructuralDiff:
interface StructuralDiff {
missing: string[]; // required elements absent from generated output
extra: string[]; // generated elements not present in spec
mismatched: Array<{ // elements present but structurally wrong
element: string;
expected: unknown;
actual: unknown;
}>;
}The diff is passed back to the Kit as KitInput.previousDiff. The Kit receives explicit, structured feedback on what to fix — not a generic "retry" instruction. This is what makes CEGIS effective: each retry is informed by the exact delta between what was produced and what was required.
Retry Architecture
Retries operate at two levels:
Node level (within a run): Each node gets up to 3 Kit execution attempts. The attempt counter increments in KitInput. On attempt 3, if the node is still not converged, it is marked FAILED in the GenerationRunSummary and excluded from downstream contract loading.
Run level (across runs): Failed nodes from a previous run are included in the next generation run's node set. The pipeline re-attempts them with fresh context — accumulated InterfaceContract data from the rest of the project may resolve dependencies that were missing before.
Stack Readiness Requirements
Any Kit can register additional convergence requirements at module initialization:
registerStackReadinessRequirement({
id: 'nextjs:api-routes-typed',
description: 'All API routes have TypeScript type annotations',
specTaskType: 'spec_endpoint',
check: async (projectId, tenantId) => { /* ... */ },
});Registered requirements are checked in parallel alongside the five generic checks. A failing stack requirement lowers the overall score and contributes a named entry to ConvergenceResult.checks with the prefix stack:.
Convergence States
| Status | Score Range | Meaning |
|---|---|---|
converged | ≥ 95 | All checks pass — generation is complete |
partially_converged | 70–94 | Most checks pass, some gaps remain |
diverged | 30–69 | Significant gaps or drift detected |
not_started | < 30 | Pipeline has not meaningfully begun |
The converged threshold of 95 (not 100) allows for edge cases in large projects where a small number of non-critical code nodes exist outside the spec — such as scaffolding utilities or dev-only tooling.