Skip to content

Generation Architecture

Codabra compiles meaning, not source code:

text
JSON config
    ↓ load + feature expansion + validation
canonical ApplicationDefinition
    ↓ TargetRenderer port
relative GeneratedFile plan
    ↓ checked filesystem adapter
Next.js / Node / PHP / Go / Symfony files

Why an intermediate model

Translating a Next.js handler into PHP or Go would copy framework accidents and produce fragile code. The canonical model instead stores stable concepts: models and fields, normalized endpoint parameters, CRUD/custom/redirect operations, authentication requirements, voters, events and views.

Business inference happens once. For example, DELETE /api/users/:id resolves to the delete User operation even when the route omits response. Every renderer receives that same operation.

Clean and hexagonal boundaries

  • core/domain contains the framework-neutral application definition.
  • core/application owns compilation and generation use cases.
  • core/ports defines the target renderer output port.
  • providers implements language/framework adapters.
  • cli is the driving adapter for files, processes, Docker and prompts.
  • Generated Node, PHP, Go and Symfony projects separate domain, application ports and infrastructure adapters.

The pipeline rejects absolute paths, .. escapes and duplicate output paths before any renderer output is written.

Why the core is not Rust

A Rust rewrite is not justified now. Generation is dominated by JSON/file I/O and template emission, not CPU work. TypeScript also shares types directly with the Node CLI, Commander, VitePress and the existing provider ecosystem. A Rust rewrite would add an FFI or subprocess boundary without removing the need for per-target renderers.

Rust becomes worth reconsidering if measurements show one of these needs:

  • very large configs make validation or planning CPU-bound;
  • a single dependency-free native binary becomes a distribution requirement;
  • untrusted third-party generators need process or WASM sandboxing;
  • incremental generation needs parallel parsing at a scale Node cannot meet.

Until a benchmark demonstrates that threshold, the clean boundary is more valuable than changing implementation language. A future Rust planner could implement the same canonical-model contract without changing configs or renderers.

Adding a target

  1. Implement TargetRenderer as a pure ApplicationDefinition → RenderResult transformation.
  2. Implement Provider for initialization, dev, build and Docker lifecycle.
  3. Register it in providerRegistry with persistence capability and port.
  4. Test paths and representative CRUD/auth output.
  5. Run lint, typecheck, tests and the generated target's native compiler.

Released under the Elastic License 2.0.