ADR-010: Rule builder for fluent DSL-style configuration
Status: Accepted
Date: 2026-05-12
Context
Most rules are configured via defineRule() with an options object. This works well for simple configuration but becomes awkward for rules that need a multi-step, interdependent setup.
The layer rule (layer('target').is(Pure).allows('a', 'b')) is the primary example. It needs:
- A target to define the boundary
- A role to determine what kind of isolation applies
- A set of allowed exceptions
Expressing this as a flat options object loses the natural flow and makes invalid configurations possible (e.g., a layer without a role).
Decision
RuleBuilder is a second extension point alongside Rule and RuleFactory:
export interface RuleBuilder {
build(): Rule;
}The CLI recognizes RuleBuilder instances in the rules array and calls .build() to produce the final Rule before registration:
if (isRuleBuilder(ruleEntry)) {
this.kernel.registerRule(ruleEntry.build());
}The defineRuleBuilder() helper brands the builder so the CLI can distinguish it from plain objects.
Builder interface
The builder is free to expose any fluent or chained API. The only requirement is that build() returns a valid Rule. The layer rule uses a two-phase builder:
layer('target').is(Pure).allows('a', 'b')
// ↑ ↑ ↑
// target role allowedThis prevents invalid configurations — a layer must have a role before it can be built.
Consequences
RuleBuilderis for rules that benefit from a DSL-like syntax. Most rules should still usedefineRule()with options.- The builder is resolved at config time, not at runtime. The resulting
Ruleis what the kernel executes. - Third-party plugin authors can use
defineRuleBuilder()if their rule has complex configuration needs. - The builder pattern is additive — it doesn't replace
defineRule()ordefineRuleSet().
