Appendix A - 18. Reflection-facet substrate (tempo-gated policy nudges)
The Structure of Reflection-facet substrate (tempo-gated policy nudges) — its shape at a glance:
Facets self-register into a typed registry; a shared emitter, bound to a lifecycle event, round-robins them and emits at most one reflection per window — the aggregate ceiling no independent hook can share.
flowchart LR
F1[Facet: failure→mechanism] --> Reg[(Facet registry)]
F2[Facet: knowledge-routing] --> Reg
F3[Facet: structural-drift] --> Reg
Reg --> Emitter{{Shared emitter}}
Emitter -->|≤1 per window, round-robin| Nudge([One paced reflection])
Accessible description: several policy facets self-register into a typed registry; a shared emitter bound to a lifecycle event round-robins them and emits at most one reflection per window, so the family shares one anti-overwhelm ceiling instead of each hook firing on its own.
Projected from the catalogue entry lifecycle-and-observability / Reflection-facet substrate (tempo-gated policy nudges).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Consolidate the operator's policy-reflection nudges into ONE tempo-gated substrate: a registry of Template-Method facets, each reflecting the running context against a single repo-policy dimension it references (never copies), the whole family emitting at most one reflection per window, so several soft reflections can't compound into the alarm fatigue that would kill them all (our instance: a ReflectionFacet base + facet registry with four facets over a shared turn-end window: failure→mechanism, knowledge-routing, structural-drift, operations).
Motivation
A single lifecycle hook that re-arms one omitted reflex is cheap and clear. The trouble starts at the second one. Once you want the operator to reflect on more than one recurring policy (convert this recurrence into a mechanism, route this lesson to the right store, this looks like a second copy, this runbook is stale), the naive path is N independent hooks, each firing on its own event.
Three failures follow, and they compound:
- Alarm fatigue kills the whole family. N soft nudges a turn is noise; the operator learns to tune them all out, and every facet dies together, the tower-of-governance the nudges were meant to prevent.
- The machinery duplicates. Each hook re-implements the same tempo gate, default-silence bias, dedupe window, and telemetry: N twins that drift apart.
- The policy rots inside the hook. Each hook bakes its rule into a payload string, so when the canonical policy doc moves or changes, the nudge silently reflects a stale rule.
Applicability
- The single-hook primitive. A runtime with lifecycle-event hooks for the emitter to bind to (lifecycle-hooks).
- A canonical, referenceable policy corpus. The docs or registries each facet points at, with a resolvable ref grammar, so a facet can reference its rule rather than restate it.
- More than one reflection policy worth nudging. A single facet does not need a registry or a shared budget; the substrate earns its keep at the second facet (extract-on-the-second-site). Built for one nudge, it is pure overhead.
Structure
The Structure diagram appears at the top of this page.
Sample Code
A Template-Method base fixes the reflect sequence; a facet fills only the sanctioned steps and declares a pointer to its policy (never a copy). A shared emitter round-robins the registered facets and emits at most one nudge per window — the ceiling the whole family shares.
from abc import ABC, abstractmethod
class ReflectionFacet(ABC):
key: str
policy_ref: str # a resolvable pointer into the canonical policy — referenced, not copied
@abstractmethod
def warrants(self, ctx) -> bool: ... # is this facet's moment? bias hard toward False (silence)
@abstractmethod
def payload(self, ctx) -> str: ... # the conservative nudge, worded from policy_ref
class Emitter:
def __init__(self, facets): self._facets, self._cursor = list(facets), 0
def tick(self, ctx) -> str | None:
n = len(self._facets)
for i in range(n): # round-robin so no single facet monopolizes the window
f = self._facets[(self._cursor + i) % n]
if f.warrants(ctx):
self._cursor = (self._cursor + i + 1) % n
return f.payload(ctx) # AT MOST ONE reflection per window across the whole family
return None
Consequences
- Facets compete for the window. The shared budget means no facet is guaranteed to fire on every eligible event: a busy tempo class round-robins. That is the anti-overwhelm working, but it trades per-facet promptness for aggregate calm.
- The closed surface is a real constraint. A facet that genuinely needs a new capability can't hack it locally: it needs a new declared virtual on the base. Deliberate (it keeps every facet uniform), but it makes the base a coordination point.
- It is over-engineering at N=1. The registry, the Template Method, and the shared budget are all overhead until there are at least two facets to consolidate; do not stand it up speculatively.
Example use within DocAble
- A
ReflectionFacetTemplate-Method base + a typed facet registry, with four facets on it: failure→mechanism (convert a recurring failure into a mechanism), knowledge-routing (a lesson belongs in memory vs a durable runbook), structural-drift (a second copy, i.e. DRY), operations (a runbook/playbook gap). Two are built, two interface-ready. - A shared turn-end emitter that round-robins the facets of a tempo class and emits ≤1 reflection per window; a separate memory-write emitter for the routing facet.
- A closed-surface lint (each facet implements exactly the sanctioned steps), a pointer-resolve lint (every facet's policy-material pointer resolves), and a per-firing telemetry log + yield query (the measured leash). The facet registry is itself modeled as a typed node behind a drift gate (model = territory).
- A runnable, portable form of the whole substrate ships as a stdlib-only, copy-and-adapt library with this catalogue's operating skill: the Template-Method base, the once-per-window emitter, two generic example facets, and the measured-leash query. It is the runnable complement to the pattern described here.
Related Patterns
- Specializes — lifecycle-hooks: the single-hook primitive this is built on. That entry's measured leash is where the family's evidence discipline lives; this entry is the registry-of- facets you reach for once a second reflection hook appears and N independent nudges would fatigue.
- Temporal complement — dynamic-context-injection: injection pushes policy into an agent at entry (feed-forward); a reflection facet pulls the operator back to policy at tempo (feed-back). The per-facet policy-material pointer is the concrete form of "both are modalities over one policy source of truth."
- See also — meta-model-consumption: a facet references its canonical policy material through a resolved pointer rather than copying it: read-don't- hardcode applied to the reflection substrate.