koder_shadow_kit
- Path:
engines/sdk/koder_shadow_kit - Kind: Go SDK library (dependency-free; stdlib only)
- Spec:
meta/docs/stack/rfcs/stack-RFC-011-shadow-parallel-run-validation.kmd(ratified 2026-06-06) - Backlog prefix:
SHADOW
Role in the stack
The shadow / parallel-run validation harness — Tier-1 of stack-RFC-011. It lets a risky new backend implementation run in shadow of the authoritative one against production traffic: the authoritative path is always what the caller gets, while the candidate (shadow) path runs in parallel and is compared, never trusted, until parity is proven. This retires the gated test window (which serializes work across the Stack) as the default mechanism for data-risky changes — validation becomes continuous and non-blocking.
It operationalizes always-on.kmd R3.1 (expand→migrate→contract) by adding the automated read-compare + promotion gate that the manual discipline lacked, and its promotion gate mirrors the G1/G2 parity gates of self-hosted-first.kmd.
Invariants (RFC §5)
- I1 Authoritative independence — the candidate never blocks, delays, or fails
the authoritative call. It runs asynchronously on a
context.WithoutCancelcontext, panic-guarded; its errors/panics are logged, never surfaced. - I2 Shadow never trusted —
Comparealways returns the control value,even on divergence. Promotion (flipping authority) is a separate gated decision.
- I3 Side-effect-free only — shadow only PURE storage/transform paths, never
operations with external side effects (email, billing, third-party writes).
API surface
| Symbol | Purpose |
|---|---|
Harness + New(Config, …Option) |
Non-generic config/sinks holder. Config{Enabled, SampleRate} (zero value = disabled pass-through). |
Compare[T](ctx, h, name, control, candidate, cmp) (T, error) |
Read-style: runs control sync (returns it); when enabled+sampled+control-ok, runs candidate async and compares. |
(*Harness) Shadow(ctx, name, control, candidate) error |
Write-style: control authoritative; candidate mirrored best-effort async (only on control success). |
Comparator[T] + EqualDeepEqualSemantic(SemanticOptions) |
Comparison. Semantic = JSON-canonical recursive compare with IgnorePaths (dot-path + * wildcard), OrderInsensitive, FloatTolerance. |
Kinds KindEqualKindValueMismatchKindShapeMismatch |
Bounded divergence taxonomy — safe as a metric label. |
Gate + NewGate(PromotionPolicy{MinSamples, MinElapsed}, …) → Verdict() PromotionVerdict |
Telemetry-driven promotion gate (G-parity: 0 divergence over ≥N samples AND ≥T soak). WithGate(g) auto-feeds it from Compare. |
| Options | WithOnDivergence, WithSampler, WithLogger, WithGate, WithClock (gate). |
| Metric-name constants | MetricDivergenceTotal (shadow_divergence_total{component,divergence_kind}), MetricShadowReadTotal — the obs contract; the consumer emits via OnDivergence (kit stays dep-free). |
Adoption sketch
gate := shadow.NewGate(shadow.DefaultPromotionPolicy())
h := shadow.New(shadow.Config{Enabled: flag, SampleRate: 0.05},
shadow.WithGate(gate),
shadow.WithOnDivergence(func(d shadow.Divergence) { }))
val, err := shadow.Compare(ctx, h, "store.read",
func(ctx) (Row, error) { return oldStore.Read(ctx, k) }, // authoritative
func(ctx) (Row, error) { return newStore.Read(ctx, k) }, // candidate
shadow.Semantic[Row](shadow.SemanticOptions{IgnorePaths: []string{"updated_at"}}))
// val/err are always the authoritative result; divergences flow to the gate/sink.
// Promote (flip authority) only when gate.Verdict().Ready.Status
- Tier-1 (this module) — Phase 1 COMPLETE (SHADOW-001..003, 2026-06-06):
core harness, semantic comparator, promotion gate. Verified off-laptop (dev-linux-id): build/vet clean,
go test -racegreen (21 tests). - SHADOW-004 dogfood — mechanism proven in-package (
dogfood_test.go); thelive soak is
gated_by: evidence-soak(needs a chosen prod consumer). - Tier-2 (kdb-native, RFC §4.2) — SHADOW-005, pending in
infra/data/kdb(Rust): a shadow applier materializes a new storage format from the same WAL (reusing the RFC-008
WalApplier, #776) and compares — authoritative write path untouched, zero hot-path cost, zero data-loss risk. - Phase 3 — SHADOW-006: update
always-on.kmdso CONTRACT-class/data-riskychanges default to the shadow gate; the window becomes the exception.
Primary couplings
engines/sdk/go/obs— the consumer wiresOnDivergence+ the sample counter tothe obs SDK (REDmetricslogs). The kit itself takes no obs dependency.
infra/data/kdb(RFC-008 WAL) — the Tier-2 substrate (#005).always-on.kmd,self-hosted-first.kmd— the policies this primitiveoperationalizes.