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 → REGISTER

No 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.metadata are 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 strategy
  • EndpointKit: REST vs RPC vs streaming based on operationType
  • SchemaKit: 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_criterion nodes
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:

  1. Generated files are written to the .generated/ directory
  2. A generated_from edge is written from the impl node to the spec node in the graph
  3. The impl node status is updated to IMPLEMENTED
  4. If the Kit declared dependencies in its KitOutput, they are merged into the project's package.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

PhaseDeterministicLLM-Assisted
CHECKAlwaysNever
SPECAlways (for known metadata shapes)First-time metadata normalization
PLANAlways (rule-based strategy)Never
APPLYSimple Kits (schema, naming, child tables)Complex Kits (components, pages, services)
TESTAlways (template-rendered)Never
VERIFYAlways (structural parse)Never
REGISTERAlwaysNever

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.

Command Palette

Search for a command to run...