@@ -90,6 +90,47 @@ class MyAdapter(BenchmarkAdapter):
9090 """
9191
9292
93+ # --------------------------------------------------------------------------- #
94+ # Overfit-dimensions schema #
95+ # --------------------------------------------------------------------------- #
96+
97+
98+ class OverfitDimensions (BaseModel ):
99+ """Metadata keys that overfit guards use to extract case attributes.
100+
101+ The framework's overfit module (``_framework/overfit.py``) runs
102+ five guards that attribute lift across (a) the corpus's "system"
103+ dimension, (b) the corpus's "stratum" / fault-category dimension,
104+ and (c) per-case ground-truth objects. Different adapters emit
105+ cells with different metadata layouts — CloudOpsBench uses
106+ ``metadata["system"]`` / ``metadata["fault_category"]`` /
107+ ``metadata["ground_truth"]["fault_object"]``; another adapter may
108+ use different key names.
109+
110+ This model lets each adapter declare its key names without
111+ requiring the framework to know about them. The defaults match
112+ CloudOpsBench's schema so existing call sites continue to work;
113+ other adapters override ``BenchmarkAdapter.overfit_dimensions``
114+ to point at their own keys.
115+
116+ Phase 3 of the framework decoupling: the previous overfit guards
117+ hardcoded ``c["case"]["metadata"]["system"]`` etc. inline, which
118+ silently coupled the framework to CloudOpsBench's metadata shape.
119+ """
120+
121+ model_config = ConfigDict (frozen = True , extra = "forbid" )
122+
123+ system_key : str = "system"
124+ """``case.metadata[<key>]`` — the corpus's "system" attribute."""
125+
126+ stratum_key : str = "fault_category"
127+ """``case.metadata[<key>]`` — the corpus's stratum / category attribute."""
128+
129+ gt_object_key : str = "fault_object"
130+ """``case.metadata["ground_truth"][<key>]`` — the GT object name used
131+ by Guard C's cluster fingerprinting."""
132+
133+
93134# --------------------------------------------------------------------------- #
94135# The adapter interface #
95136# --------------------------------------------------------------------------- #
@@ -137,6 +178,47 @@ def apply_config_overrides(self, config: Any) -> None: # noqa: ARG002 — defau
137178 """
138179 return None
139180
181+ def overfit_dimensions (self ) -> OverfitDimensions :
182+ """Metadata keys the overfit guards should consult for this adapter.
183+
184+ Default returns ``OverfitDimensions()`` which matches the
185+ CloudOpsBench schema (``system``, ``fault_category``,
186+ ``ground_truth.fault_object``). Adapters with a different
187+ metadata layout override this to point the guards at their own
188+ keys.
189+
190+ Phase 3 of the framework decoupling: previously
191+ ``_framework/overfit.py`` indexed into these keys inline,
192+ coupling the framework to one adapter's metadata shape. This
193+ hook moves the schema declaration to the adapter that owns it.
194+ """
195+ return OverfitDimensions ()
196+
197+ def extend_provenance (self , provenance : dict [str , Any ]) -> dict [str , Any ]:
198+ """Optional: inject adapter-specific fields into the provenance dict.
199+
200+ Called by ``_framework/provenance.py::capture_provenance`` after
201+ the framework finishes assembling its standard sections
202+ (``code``, ``config``, ``pre_registration``, ``models``,
203+ ``environment``, ``dataset``, ``run_inputs``). Adapters can:
204+ - add a new top-level key (e.g. an adapter-specific run note)
205+ - extend an existing section (e.g. add ``min_tool_calls`` to
206+ ``run_inputs``)
207+ - return the dict unchanged
208+
209+ Default is identity. The hook exists so the framework's
210+ provenance module stays adapter-agnostic — it does NOT import or
211+ reach into any adapter's internals to build the artifact. Before
212+ Phase 4 the framework imported ``cloudopsbench.bench_agent``
213+ directly to read ``min_tool_calls``; that import is gone and
214+ CloudOpsBench now overrides this hook instead.
215+
216+ Implementations should mutate-and-return for performance, or
217+ return a fresh dict if a non-destructive transform is needed —
218+ the framework respects the return value either way.
219+ """
220+ return provenance
221+
140222 @abstractmethod
141223 def load_cases (self , filters : CaseFilters ) -> Iterator [BenchmarkCase ]:
142224 """Stream cases matching the filter. Seeded random selection is the
0 commit comments