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 scratch

The score is a weighted composite across five named checks:

CheckWeightPasses When
entityCoverage30%≥ 80% of spec entity nodes have implementations
endpointCoverage25%≥ 80% of spec_endpoint nodes are implemented
guardCoverage20%Unguarded endpoint count produces score ≥ 80
operationCoverage15%≥ 80% of spec_operation nodes are implemented
driftCheck10%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

StatusScore RangeMeaning
converged≥ 95All checks pass — generation is complete
partially_converged70–94Most checks pass, some gaps remain
diverged30–69Significant gaps or drift detected
not_started< 30Pipeline 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.

Command Palette

Search for a command to run...