Interface Contracts

How typed handoffs work between Kit phases — the InterfaceContract type, what it contains, and how downstream Kits consume upstream contracts without parsing generated files.

Interface Contracts

When a Kit generates a module, it writes an Interface Contract — a structured description of what the module exports. Downstream Kits read these contracts to understand what their dependencies provide, without needing to parse source files or make assumptions about naming conventions.

Interface Contracts are the typed communication channel between Kits.


The KitInput/KitOutput Contract

Every Kit receives a KitInput and returns a KitOutput. These interfaces define the boundary between the generation pipeline and each Kit:

interface KitInput {
  /** The impl node ID from context_artifacts */
  nodeId: string
  /** The impl node type (e.g., 'impl_entity', 'impl_operation') */
  implNodeType: string
  /** Human-readable label for logging */
  label: string
  /** The spec node this Kit is implementing */
  specNode: ContextArtifact
  /** What upstream modules expose — loaded from graph before Kit runs */
  upstreamContracts: InterfaceContract[]
  /** Best-matching pattern from the flywheel, if any */
  patternSeed?: PatternMatch
  /** Feedback from previous failed attempt */
  previousDiff?: StructuralDiff
  /** Retry attempt number (1-indexed) */
  attempt?: number
  /** Whether this is a restart of a previously failed run */
  isRestart?: boolean
  /** Kit type identifier for routing */
  kitType?: KitType
  /** Project tech stack and conventions */
  projectContext: ProjectContext
}

interface KitOutput {
  /** Generated source files */
  files: Array<{ path: string; content: string }>
  /** Generated test files */
  testFiles: Array<{ path: string; content: string }>
  /** What this module exports — consumed by downstream Kits */
  exportedInterface: InterfaceContract
}

The upstreamContracts field in KitInput and the exportedInterface field in KitOutput are the two sides of the typed handoff.


The InterfaceContract Type

An InterfaceContract describes what a generated module exposes to the rest of the project:

interface InterfaceContract {
  /** The impl node ID this contract describes */
  nodeId: string
  /** The Kit type that produced this contract */
  producedBy: KitType
  /** Exported TypeScript functions with their signatures */
  exportedFunctions: Array<{
    name: string
    signature: string        // e.g., "(id: string) => Promise<User>"
    isAsync: boolean
    parameters: Array<{ name: string; type: string }>
    returnType: string
  }>
  /** Exported TypeScript types and interfaces */
  exportedTypes: Array<{
    name: string
    definition: string       // TypeScript type expression
    isExported: boolean
  }>
  /** API routes this module exposes (for EndpointKit) */
  apiRoutes?: Array<{
    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
    path: string
    requestType?: string
    responseType?: string
    authRequired: boolean
  }>
  /** Events this module emits or consumes */
  events?: Array<{
    name: string
    direction: 'emits' | 'consumes'
    payloadType: string
  }>
  /** Database entities this module exposes */
  dbEntities?: Array<{
    tableName: string
    schemaRef: string        // import path to Drizzle schema
    primaryKey: string
  }>
  /** Module import path (for downstream import statements) */
  importPath: string
}

How Upstream Contracts Are Loaded

Before the pipeline executes a Kit for node N, loadUpstreamContracts(nodeId) traverses the spec graph:

  1. Find all nodes that N depends on by following has_field, has_operation, implements, and uses edges backward
  2. For each dependency node, retrieve the InterfaceContract stored in its impl node's metadata
  3. Return the contracts as upstreamContracts in the KitInput

This traversal is lazy — it runs immediately before each Kit execution, ensuring it picks up contracts from the current run's completed nodes, not just contracts from previous runs.


How Kits Use Upstream Contracts

A Kit uses upstream contracts to generate compatible code without guessing:

EndpointKit reads ServiceKit's exported functions to generate route handlers:

// From ServiceKit's InterfaceContract:
// exportedFunctions: [{ name: 'getUserById', signature: '(id: string) => Promise<User>' }]

// EndpointKit generates:
import { getUserById } from '../services/UserService'

app.get('/users/:id', async (c) => {
  const user = await getUserById(c.req.param('id'))
  return c.json(user)
})

ServiceKit reads EntityKit's exported types to generate typed repository methods:

// From EntityKit's InterfaceContract:
// exportedTypes: [{ name: 'User', definition: '{ id: string; email: string; name: string }' }]
// dbEntities: [{ tableName: 'users', schemaRef: '../db/schema/users', primaryKey: 'id' }]

// ServiceKit generates:
import { users } from '../db/schema/users'
import type { User } from '../entities/User'

async findById(id: string): Promise<User | null> {
  const [row] = await db.select().from(users).where(eq(users.id, id))
  return row ?? null
}

APIClientKit reads EndpointKit's API routes to generate a typed client:

// From EndpointKit's InterfaceContract:
// apiRoutes: [{ method: 'GET', path: '/users/:id', responseType: 'User', authRequired: true }]

// APIClientKit generates:
export async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`, {
    headers: { Authorization: `Bearer ${getToken()}` },
  })
  return response.json()
}

Contract Storage

Contracts are stored in the impl node's metadata in the context_artifacts table. After a Kit's REGISTER phase completes, the pipeline:

  1. Writes the exportedInterface to context_artifacts.metadata->>'interfaceContract'
  2. Writes a generated_from edge from the impl node to its spec node
  3. Makes the contract available for subsequent loadUpstreamContracts calls

If a node fails convergence after 3 attempts, its contract is not written to the graph. Downstream Kits that depend on a failed node will have a gap in their upstreamContracts array. The pipeline handles this by generating a minimal stub contract for failed nodes, allowing the run to continue rather than cascading failures across the entire dependency tree.


StructuralDiff Feedback

When a Kit's output fails CEGIS verification, the StructuralDiff is passed back as KitInput.previousDiff on the retry:

interface StructuralDiff {
  missing: string[]      // required elements absent from output
  extra: string[]        // elements not in spec but present in output
  mismatched: Array<{    // elements present but structurally wrong
    element: string
    expected: unknown
    actual: unknown
  }>
}

LLM Kits include the diff in their generation prompt, giving the model explicit instruction on what to correct. Deterministic pipeline steps that fail CEGIS are treated as implementation bugs — the diff is logged but not passed back to the step.

Command Palette

Search for a command to run...