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:
- Find all nodes that N depends on by following
has_field,has_operation,implements, andusesedges backward - For each dependency node, retrieve the
InterfaceContractstored in its impl node's metadata - Return the contracts as
upstreamContractsin theKitInput
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:
- Writes the
exportedInterfacetocontext_artifacts.metadata->>'interfaceContract' - Writes a
generated_fromedge from the impl node to its spec node - Makes the contract available for subsequent
loadUpstreamContractscalls
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.