Kit Execution
The 7-phase lifecycle every Kit follows — from pre-flight checks through registration — with deterministic vs LLM breakdown.
Kit Execution
A Kit is the unit of code generation in Praetor. Each Kit is responsible for one type of implementation node (e.g., impl_entity, impl_endpoint, impl_ui_component) and produces a KitOutput containing generated files.
Every Kit follows the same 7-phase lifecycle:
CHECK → SPEC → PLAN → APPLY → TEST → VERIFY → REGISTERNo Kit skips a phase. The pipeline enforces this order.
KitInput and KitOutput
Every Kit receives a KitInput and must return a KitOutput. These interfaces are the contract between the generation pipeline and the 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 */
label: string
/** The structured metadata from the impl node — shape varies by type */
metadata: Record<string, unknown>
/** The spec node ID this impl realizes (for traceability) */
specNodeId: string | null
/** IDs of related nodes for cross-referencing */
contextRefs: string[]
/** Project-level conventions document (optional) */
conventions?: string
}
interface KitOutput {
/** Generated files */
files: GeneratedFile[]
/** Summary of what was generated */
summary: string
/** Any warnings (non-fatal) */
warnings: string[]
/** npm dependencies required by generated code from this Kit */
dependencies?: Record<string, string>
/** npm devDependencies required by generated code from this Kit */
devDependencies?: Record<string, string>
}The GeneratedFile type carries a relative path (within .generated/) and the file content as a string:
interface GeneratedFile {
/** Relative path within .generated/ (e.g., 'src/db/schema/tasks.ts') */
path: string
/** File content */
content: string
}The 7-Phase Lifecycle
Phase 1 — CHECK
Deterministic. The Kit verifies that all prerequisites are satisfied before attempting any generation.
What is checked:
- All upstream dependencies (spec nodes this node depends on) have status
IMPLEMENTED - Required fields in
input.metadataare present and well-formed - There are no unresolved ambiguities that would cause generation to fail silently
If any check fails, the Kit returns early with a warnings entry explaining the blockage. The node is placed back in the queue and retried once its dependencies are satisfied. This prevents partial-output states where a generated file references a module that has not been generated yet.
Phase 2 — SPEC
Deterministic (with LLM-assisted extraction on first-time projects). The Kit reads the structured metadata from input.metadata and optionally loads referenced nodes from input.contextRefs.
This phase normalizes the metadata into a canonical shape the rest of the Kit can work with. For example, an EndpointKit in the SPEC phase resolves the upstream ServiceKit output path so it can write the correct import statement.
For well-known metadata shapes (ERPNext DocType JSON, Frappe child tables, Zod schema definitions), SPEC is fully deterministic — no LLM involved.
Phase 3 — PLAN
Deterministic (rule-based strategy selection). Based on the normalized metadata from SPEC, the Kit selects a generation strategy.
Examples:
ComponentKit:componentType: 'table'→ TanStack Table strategy;componentType: 'form'+isModal: true→ Dialog form strategyEndpointKit: REST vs RPC vs streaming based onoperationTypeSchemaKit: normalized vs denormalized based on field cardinality rules
The PLAN phase produces a strategy token that guides APPLY. It does not generate files — it selects the template branch.
Phase 4 — APPLY
LLM-assisted (bounded by grammar and strategy). This is the generation step. The Kit uses the strategy selected in PLAN and the metadata normalized in SPEC to produce file content.
Kits with deterministic output (e.g., NamingSeriesKit, ChildTableKit, SchemaKit for simple schemas) do not call an LLM in APPLY — they render from templates directly. The CodegenKit interface makes no distinction: generate(input: KitInput): KitOutput is always synchronous and deterministic in the current implementation.
When an LLM is involved (for narrative-heavy content, complex business logic, or multi-entity orchestration), the output is constrained by grammar rules that prevent structurally invalid code from being returned.
Phase 5 — TEST
Deterministic (template-rendered). The Kit optionally generates test files alongside the implementation files.
The CodegenKit interface includes an optional generateTests method:
interface CodegenKit {
readonly implNodeType: string
readonly name: string
generate(input: KitInput): KitOutput
generateTests?(input: KitInput): KitTestOutput[]
}Test files follow Vitest syntax and cover:
- Type-level contract checks (does the generated schema export the expected types?)
- Behavioral contracts (does a route handler respond with the correct shape?)
- Edge cases specified in
spec_acceptance_criterionnodes
interface KitTestOutput {
/** Relative path for the test file */
testFile: string
/** Full test file content using Vitest syntax */
testCode: string
/** spec_acceptance_criterion artifact_keys this test covers */
coversCriteria: string[]
/** Code node artifact_keys this test validates */
validatesCode: string[]
}Phase 6 — VERIFY
Deterministic. After APPLY produces file content, a structural verifier checks the output against the spec node's requirements before the files are committed to disk.
Verification checks vary by Kit type:
- EntityKit: all required fields present in the Drizzle schema; types match spec
- EndpointKit: route path matches spec; method matches; auth guard wired correctly
- ComponentKit:
'use client'present; component name exported; all referenced entity types imported
If verification fails, the pipeline computes a StructuralDiff and retries via the CEGIS loop (up to 3 attempts). The diff is passed back into KitInput as previousDiff on retry, giving the generation step explicit feedback on what to fix.
Phase 7 — REGISTER
Deterministic. Once verification passes, the Kit's output is committed:
- Generated files are written to the
.generated/directory - A
generated_fromedge is written from the impl node to the spec node in the graph - The impl node status is updated to
IMPLEMENTED - If the Kit declared
dependenciesin itsKitOutput, they are merged into the project'spackage.json
Registration is idempotent — re-running generation for an already-implemented node re-checks and overwrites only if the content has changed.
Phase flow diagram
flowchart TD
A[KitInput received] --> B{CHECK\nDeterministic}
B -->|prerequisites missing| Z[Return early\nwarnings only]
B -->|all clear| C[SPEC\nNormalize metadata]
C --> D[PLAN\nSelect strategy]
D --> E{APPLY\nGenerate files}
E -->|deterministic templates| F[TEST\nGenerate test files]
E -->|LLM-bounded by grammar| F
F --> G{VERIFY\nStructural check}
G -->|pass| H[REGISTER\nWrite files\nUpdate graph]
G -->|fail\nattempt < 3| I[CEGIS retry\nStructuralDiff → KitInput]
I --> E
G -->|fail\nattempt = 3| J[Mark node FAILED\nContinue pipeline]
H --> K[KitOutput returned]Kit Registry
The Kit Registry maps implNodeType strings to Kit implementations. When the generation pipeline encounters an impl node, it calls getKit(implNodeType) to retrieve the correct Kit.
// Registration (each Kit module calls this at import time)
export function registerKit(kit: CodegenKit): void
// Lookup
export function getKit(implNodeType: string): CodegenKit | null
// Initialization — call once at startup
export async function initializeKits(): Promise<void>Kits self-register by calling registerKit(new MyKit()) at the bottom of their module file. initializeKits() triggers all dynamic imports in dependency order. After it resolves, every registered Kit is available via getKit().
The registry is a plain Map<string, CodegenKit> — no reflection, no decorators, no magic. The implNodeType string is the single key.
Multi-node Kits
Some Kits need to see all nodes of their type at once rather than processing one at a time. For example, NavigationKit must read all impl_ui_page nodes to build a coherent navigation tree.
These Kits implement MultiNodeCodegenKit:
interface MultiNodeCodegenKit extends CodegenKit {
generateMulti(inputs: KitInput[]): KitOutput
generateMultiTests?(inputs: KitInput[]): KitTestOutput[]
}The pipeline detects multi-node Kits via the isMultiNodeKit type guard and batches all nodes of that type into a single generateMulti call instead of calling generate per node.
What is deterministic vs LLM
| Phase | Deterministic | LLM-Assisted |
|---|---|---|
| CHECK | Always | Never |
| SPEC | Always (for known metadata shapes) | First-time metadata normalization |
| PLAN | Always (rule-based strategy) | Never |
| APPLY | Simple Kits (schema, naming, child tables) | Complex Kits (components, pages, services) |
| TEST | Always (template-rendered) | Never |
| VERIFY | Always (structural parse) | Never |
| REGISTER | Always | Never |
The goal of the architecture is to maximize the deterministic surface and minimize LLM involvement to only the creative/structural portion of APPLY where it genuinely adds value.