Stack Coverage
Where each primitive applies, where the gaps are.
Layer-by-layer assessment
| Layer | Derived obligations | Prescriptive failure | Bundled enforcement | Vacuity detection |
|---|---|---|---|---|
| Database schema | Schema IS the spec | Migration fails if invalid | Typed query results | Golden file for schema |
| Data access | sqlc from SQL | Shows expected signature | Typed return values | N/A (generated code) |
| Business logic | Gap, no codegen | Harder here (see below) | Method signatures | Property-based testing |
| API layer | Huma from structs | Generated OpenAPI shows contract | Validated typed input | Schema validation |
| Frontend types | openapi-typescript | TS compiler errors | Types enforce API contract | Snapshot tests |
| Tests | Spec-derived requirements | Panic names the method to call | Data bundled with enforcement | Blank-node test, golden files |
| CI/CD | Pipeline from config | Specific remediation messages | Pipeline won't pass without checks | File size limits, coverage gates |
| Config | Typed struct | "missing field X of type Y" | Config struct is only access path | Zero-value detection |
The business logic gap
Business logic is the weakest layer. There is no codegen pipeline for "derive correct business logic from spec." The LLM has to reason about it. This is where 83% of LLM errors live, the logic errors that no compiler catches.
Mitigations (weaker than the primitives, but they help):
Exhaustive enum matching. In Rust, match is exhaustive by default.
In Go, use the exhaustive linter for switch statements. When you add
a new status value, every switch that doesn't handle it fails CI. This
turns a class of logic errors into type errors.
Strong domain types. type UserID int64 and type OrderID int64
are different types in Go. The compiler prevents passing one where the
other is expected. This narrows what the LLM can express.
Property-based testing. rapid in Go, proptest in Rust. Instead
of writing examples, declare invariants ("output is sorted," "output is
a subset of input"). The framework generates thousands of inputs and
checks every one. Brief, binary, verifiable failure when an invariant
breaks.
Small, pure functions. Minimize the reasoning surface per unit. A 20-line pure function with typed inputs and outputs is easier for the LLM to get right than a 100-line method with side effects.
The derivation chain
The ideal architecture is a chain where each layer's output is derived from the layer above:
SQL schema (hand-written)
-> sqlc generates Go query functions
-> Huma derives OpenAPI from handler structs
-> openapi-typescript generates TS types
The LLM writes SQL and Go structs. Three layers of type-safe code are computed. If the SQL is wrong, sqlc refuses to generate. If the struct is wrong, Huma refuses to register. Each arrow is a layer where the LLM cannot produce wrong output.
The chain breaks at business logic. Between "data access" and "API layer," the LLM has to write the logic that transforms data into responses. That's the reasoning surface. The primitives narrow the inputs and outputs of that surface; they can't eliminate the reasoning itself.
Coverage by language
| Principle | Go | Rust |
|---|---|---|
| Derived obligations | sqlc, Huma, buf | diesel/sqlx codegen, tonic, axum |
| Prescriptive failure | Custom error messages, golangci-lint | Compiler errors with codes, clippy |
| Bundled enforcement | t.Cleanup, typed returns | Drop trait, MustInspect, typestate |
| Vacuity detection | Golden files, property testing | Blank-node pattern, property testing |
Rust has a stronger type-system story for bundled enforcement (the Drop trait is built in; Go requires discipline). But Rust's verbose errors (15-30 lines per borrow checker message) accelerate the debugging decay that DDI measured. Go's terser errors align better with the prescriptive failure principle.
Both languages can implement all four primitives. The tooling differs but the architectural patterns are the same.