Emitter Router

How the convergence router decides, for each project, which emitter target to use — Config Kit, Codegen Kit, or both — and what that means for generation.

Emitter Router

The emitter router is the entry point for convergence checking. For each project, it reads the project's emitter_targets column and dispatches convergence checks to the appropriate emitter-specific checker. The result is a ProjectConvergenceResult that aggregates outcomes across all active targets.


Core Types

// emitter-convergence-types.ts

export interface EmitterConvergenceResult {
  emitterTarget: string;
  isDone: boolean;
  conditions: Record<string, boolean>;
  gaps: string[];
}

export interface ProjectConvergenceResult {
  projectId: string;
  overallDone: boolean;
  byEmitter: EmitterConvergenceResult[];
}

EmitterConvergenceResult.conditions is a named map of every convergence check that ran, keyed by check name. gaps contains the details string for each check that did not pass — making it directly actionable.


Routing Decision Logic

The router reads emitter_targets from the projects table. If the column is null or empty it defaults to ['config']:

// convergence-router.ts

export async function checkProjectConvergence(
  projectId: string,
  tenantId?: string
): Promise<ProjectConvergenceResult> {
  const rows = await sql<Array<{ emitter_targets: string[] }>>`
    SELECT emitter_targets FROM projects WHERE id = ${projectId}::uuid
  `;
  if (rows.length === 0) throw new Error('Project ' + projectId + ' not found');
  const targets: string[] = rows[0].emitter_targets ?? ['config'];
  const results: EmitterConvergenceResult[] = [];

  for (const target of targets) {
    if (target === 'config') {
      results.push(await checkConfigConvergence(projectId));
    } else if (target === 'codegen') {
      results.push(await checkCodegenConvergence(projectId, tenantId ?? ''));
    } else {
      results.push({
        emitterTarget: target,
        isDone: false,
        conditions: {},
        gaps: ['Unknown emitter target: ' + target],
      });
    }
  }

  return {
    projectId,
    overallDone: results.every(r => r.isDone),
    byEmitter: results,
  };
}

overallDone is only true when every emitter target reports isDone: true. A project with targets ['config', 'codegen'] must satisfy both checkers before it is considered complete.


Emitter Targets

config

Routes to checkConfigConvergence. Used for projects that emit configuration files — environment setup, infrastructure manifests, deployment config. The config convergence check verifies that the generated configuration matches the spec's environment and dependency requirements.

codegen

Routes to checkCodegenConvergence, which calls the full checkConvergence function from convergence-checker.ts. This runs the 5-check weighted scoring pipeline:

// codegen-convergence.ts

export async function checkCodegenConvergence(
  projectId: string,
  tenantId: string
): Promise<EmitterConvergenceResult> {
  const result = await checkConvergence(projectId, tenantId);
  return {
    emitterTarget: 'codegen',
    isDone: result.status === 'converged',
    conditions: Object.fromEntries(result.checks.map(c => [c.name, c.passed])),
    gaps: result.checks.filter(c => !c.passed).map(c => c.details),
  };
}

The conditions map flattens the full ConvergenceCheck[] array into a boolean record. The gaps array contains human-readable descriptions of every failing check.

Unknown targets

Any unrecognized emitter target produces a result with isDone: false and a gap entry of 'Unknown emitter target: {target}'. This prevents silent failures when a project is misconfigured — the convergence result will surface the unknown target as an explicit gap.


Per-Node vs Per-Project

The router operates at the project level, not the node level. It answers: "Is this entire project converged across all its emitter targets?"

Per-node convergence (whether an individual spec node has a verified implementation) is tracked separately in the graph via generated_from edges and node status fields. The emitter router aggregates those node-level facts into a project-level summary.


Integration with Codegen Pipeline

The BatchCoordinator calls checkProjectConvergence after completing each generation batch. If overallDone is false, it inspects the gaps arrays across all emitter results to determine which nodes to include in the next batch's retry set.

This means the emitter router's output is both a status signal (is generation complete?) and a diagnostic (exactly which checks are failing and why).

Command Palette

Search for a command to run...