Skip to content

D775 — A KB's policy is one immutable snapshot replaced whole, read through one accessor

Decision. The per-KB policy (artifact and template writes, write gate, auto-repair, repair on write, doctor cadence and budget, usage threshold) lives in a kb.Policy value held by the KB in an atomic.Pointer. KB.SetPolicy stores a copy; KB.Policy() returns a copy. A reader takes one snapshot per call and uses it throughout.

Why. D746 made the settings refresh run at runtime, but the policy stayed a set of plain kb.KB fields assigned one by one while kb_status, lint and the write tools read them without a lock: a data race, and a reader could see half of an old policy and half of a new one. A mutex around each field would not fix the second problem; replacing the whole value does both.

Alternatives rejected. An RWMutex over the fields: every reader must remember to lock, and the compiler cannot find the ones that do not. Keeping the refresh inside the git lock only: kb_status and the Atlas read without it.

Consequences. The policy fields are gone from kb.KB, so a new reader cannot read one without going through Policy(). Slices in a snapshot are copies; an in-place edit of a returned slice never reaches the KB. Tests change a field with p := k.Policy(); p.X = ...; k.SetPolicy(p). internal/config's KBSpec keeps its own fields of the same names: they are the configuration, not the resolved policy.