ADR 0028: Backend-enriched graph assisted slice planning
- Status: Proposed
- Date: 2026-06-19
- Decider(s): @davesade (David Kubec)
- Relates to: ADR 0006, ADR 0007, ADR 0014, ADR 0017, ADR 0025, ADR 0026, ADR 0027
Context
Kilolock already improves on plain Terraform in two important ways:
- state is normalized in PostgreSQL rather than treated as an opaque blob
klscoped workflows can run Terraform against a local slice instead ofalways applying against the full trunk
That is a real step forward, but today's slice construction still starts by fetching the full current trunk state to the client and then narrowing it locally. For large states this leaves two important problems unsolved:
- latency / transfer cost
- a 50 MB or 100 MB state still has to move over the network to the client
before
kl --file ...can narrow it
- a 50 MB or 100 MB state still has to move over the network to the client
- local working set size
- even if Terraform later runs against a smaller slice, the client already
handled the full trunk
- even if Terraform later runs against a smaller slice, the client already
This weakens one of the key product promises for large states. An operator who changes one file should not have to download an entire state graph just to let Kilolock decide that only a tiny subset was relevant.
At the same time, a naive "just fetch the resources declared in the selected file" approach is not safe. Terraform planning depends on more than file ownership:
- transitive resource dependencies
- explicit
depends_on - module boundaries
- realized
count/for_eachexpansions - provider alias wiring
- outputs, locals, and references
- objects being moved, removed, replaced, or newly added
State alone is not sufficient to reconstruct the entire future plan graph. Configuration alone is not sufficient to cheaply discover all realized graph context. We need both.
Decision
Kilolock will evolve scoped planning toward a backend-assisted slice construction model:
- enrich the realized graph persisted in PostgreSQL
- use local configuration analysis to identify intended change scope
- ask the backend for a dependency closure over the realized graph
- materialize a reduced candidate slice on the client
- fail closed or widen the slice when completeness cannot be proven
The goal is not a mathematically perfect minimal slice in all cases. The goal is a minimal safe slice for most real-world cases, with explicit fallback when proof is incomplete.
Why this is worth doing
If this works well, Kilolock gains:
- much smaller client downloads for large states
- lower latency for
kl plan -f ...andkl apply -f ... - less incidental exposure of unrelated state on operator machines
- better leverage from Kilolock's normalized graph storage
Even if some edge cases still require widening to a larger slice or full trunk, that is still a major win for the majority path.
Decision details
D1. Persist a richer realized graph in PostgreSQL
On every successful save / commit path, Kilolock should persist more than the current resource rows and raw-state reconstruction inputs.
The realized graph should be enriched with data such as:
- canonical resource instance address
- module path / module instance path
- realized
countandfor_eachinstance keys - provider reference / alias association
- dependency edges between realized instances
- reverse dependency edges
- lifecycle / ownership metadata already inferred elsewhere
- optional source-file provenance when it can be derived safely
This is a graph of the current realized state, not a promise about the next configuration graph.
D2. Scoped workflows start from local config intent
kl plan -f file.tf, kl apply -f file.tf, and future target-scoped entry points must still begin with local configuration analysis.
That local analysis identifies:
- candidate resources/modules owned by the selected files
- new resources not yet present in state
- moved / removed / import declarations
- referenced variables, locals, outputs, providers, and data sources
The local config remains the authoritative source for the intended change boundary.
D3. Backend expands that intent over the realized graph
Once the client has identified candidate targets, it should ask the backend for the smallest known realized dependency closure it can compute from PostgreSQL.
The backend may expand across:
- direct resource dependencies
- transitive dependency closure
- module prefixes / module instance containment
- provider configuration dependencies
- previously persisted ownership / provenance hints
This lets the backend return a much smaller candidate state slice than the full trunk for most changes.
D4. New resources are handled from config, not state
Resources that do not yet exist in the current state can never be discovered from PostgreSQL alone.
They must be represented by local config analysis and then merged with the backend-provided realized closure so Terraform sees:
- the new resource/module being introduced
- the realized objects it depends on
- the provider/module context needed to plan it
D5. Fail closed when slice completeness is uncertain
The backend-assisted slice is an optimization, not a correctness loophole.
If Kilolock cannot prove a candidate slice is safe enough, it must:
- widen the slice conservatively, or
- fail with an explicit diagnostic instructing the operator to run a broader
scoped plan or a full plan
Wrongly omitting required context is worse than losing the optimization.
D6. "Perfect slice" is not a requirement
We explicitly do not require a globally perfect minimal slice in all cases.
Some plan-time context is only fully known after Terraform evaluates the configuration and provider schemas. Examples include:
- data-source driven context
- unknown values that resolve only during plan
- replacement semantics and lifecycle interactions
- configuration graph changes not reflected in the previous state
This ADR aims for:
- minimal safe slice in most cases
- predictable conservative fallback in the rest
Architecture
Current model
Today, the client typically does:
- fetch full trunk
- analyze selected files / targets
- derive slice locally
- run Terraform against a local backend slice
Target model
The target flow becomes:
- client analyzes selected files / targets locally
- client sends candidate addresses / module prefixes to backend
- backend expands them over the enriched realized graph
- backend returns candidate slice payload plus dependency metadata
- client merges in local-config-only additions (new resources, moves, removals)
- client runs Terraform against the reduced local slice
- if Terraform or KL detects missing context, widen or fail closed
In shorthand:
safe_slice ≈ config_intent + realized_graph_closure + conservative_fallback
Scope of backend enrichment
The enriched PostgreSQL data should help with:
- module-aware closure expansion
- realized
for_each/countinstance discovery - reverse dependency lookups
- cheaper read/write set estimation
- future reservation precision improvements
- future repair/query UX
It should not be treated as a replacement for configuration analysis.
Consequences
Positive
- Better performance on large states without waiting for full server-side
planning.
- Stronger differentiation from blob-based backends.
- Better foundation for smarter file-scoped and target-scoped workflows.
- Reduced need to move entire trunk state to the client for common changes.
Tradeoffs
- More metadata must be persisted and kept consistent on save.
- Slice-construction logic becomes more complex.
- We still need a conservative fallback path for edge cases.
- The backend graph can be authoritative for the current realized state, but not
for every future config change.
Non-goals
- Replacing Terraform's planning engine.
- Guaranteeing a perfect minimal slice for every configuration.
- Eliminating local slice materialization entirely in this phase.
- Solving full SAML/SCIM/IAM-style auth concerns through this ADR.
Follow-up work
- Define the additional realized-graph metadata to persist on save.
- Add backend API(s) for dependency-closure slice expansion.
- Teach
kl plan -f/kl apply -fto request backend-assisted slices. - Add diagnostics that explain when KL widened or abandoned a reduced slice.
- Add large-state benchmarks comparing:
- full trunk fetch
- backend-assisted reduced slice
- full fallback path