Context
A policy tool that reconciles live infrastructure has to answer: what happens to a setting nobody
declared in the config? Terraform’s answer is “reset to default, or delete” — full desired-state
enforcement. For a tool explicitly positioned as adoptable incrementally on an already-live
repository (see README, “Not a Terraform replacement”), that default is actively dangerous: the
first apply on an existing repo could reset or delete settings a team configured by hand years
ago, with no state file to warn anyone what’s about to change.
Decision
Default behavior (“managed-scope”) never touches a branch absent from policy.yml, and within a
declared branch only ever sets the fields explicitly present — everything else is read from
current state and passed through unchanged. Full desired-state enforcement is an explicit opt-in
(strict: true, top-level default or per-branch override).
Alternatives Considered
- Always full enforcement (Terraform-style) — rejected as the default specifically because it
contradicts the “safe to adopt on a live repo” positioning; an accidental first
applyshould never be a surprise. - Always managed-scope, no strict option at all — simpler, smaller surface area, but removes a legitimate need: some teams do want full drift correction. Rejected as too limiting long-term.
Consequences
BranchPolicy’s six policy fields had to be modeled asOptional[...] = Nonespecifically so “not declared” is representable and distinguishable from “declared as false/empty” — this is the single modeling decision everything else indiff.py/apply.pyis built around (diff.resolve_desired).- Every “empty” schema default (e.g.
status_checks’s absence) has to be represented identically to what the live-API translators produce for “nothing configured,” or strict mode reports phantom drift forever — this exact bug shipped once and was only caught by testing against a real repository (seedocs/test-strategy.md). strictmode’s ruleset-pruning behavior (0004-ruleset-ownership-via-naming-convention.md) only works because ruleset ownership has a naming-convention marker; branch protection has no equivalent, so strict mode can tighten fields within a declared branch but can never fully “unprotect” a branch removed from the config — a known, documented v1 limitation (ARCHITECTURE.md).
Links
src/repo_policy/diff.py(resolve_desired,_SCHEMA_DEFAULTS)docs/test-strategy.md(the phantom-drift regression)