Compute and registry are plug-in slots. Callers supply BuildDirectives in Nickel — no caller identity in core code.
---
# Three-Tier Sizing Resolution
**First match wins. Explicit beats historical beats defaults.**
### Tier 1 — Explicit
```nix
# .build-spec.ncl
# in build context
{
runner_type = "cax31",
# authoritative
# or raw resources:
cpu = 8,
memory_gb = 16,
time_budget_min = 90,
}
```
Validated against `schemas/build_spec.ncl`.
Repo-level contract.
### Tier 2 — P95 Historical
```
GET /api/v1/p95?workspace=…
→ {
cpu_p95: 3.2, mem_p95: 6.1
}
effective = {
cpu: ceil(3.2 × 1.2) = 4,
mem_gb:ceil(6.1 × 1.2) = 8,
}
floor: min(2 cpu, 4 GB)
```
Measured from prior runs.
Advisory — operator must approve before production use.
### Tier 3 — Lang Default
```rust
match language {
"rust" => (
4 cpu, 8 GB, 60 min),
"go" => (
2 cpu, 4 GB, 30 min),
"java" => (
4 cpu, 8 GB, 45 min),
_ => (
2 cpu, 4 GB, 30 min),
}
```
Conservative floor. Rust is more expensive than Go — that's structural.
---
# OOM Retry — Bounded Escalation
**Exit 137 or stderr "OOM"/"Killed" → walk one tier up. Once.**
```rust
// MAX_OOM_RETRIES = 1 —
// ADR-039 constraint oom-retry-bounded
pub const MAX_OOM_RETRIES: u8 = 1;
const SIZE_TIERS: &[(&str, u32, u32)] = &[
("cax11", 2, 4),
("cax21", 4, 8), // ← most Rust builds here
("cax31", 8, 16), // ← OOM retry target
("cax41", 16, 32),
];
```
### Why bounded at 1
- Second OOM means **misconfiguration**,
not transient pressure
- Unbounded retry loops spend money
on dead ends
- Forces developer to set explicit `runner_type`
in `.build-spec.ncl`
- ADR-039 constraint —
changing this requires a new ADR
### Retry flow
```
build on cax21 → OOM (exit 137)
└─ retries_used(0) < MAX_OOM_RETRIES(1) ✓
└─ next_size_tier(cax21) → cax31
└─ rebuild on cax31
├─ success → record_metrics, destroy
└─ OOM again → FAIL (retries exhausted)
```
---