feat(verification): type verification_spec with roles (schema foundation) - #3
feat(verification): type verification_spec with roles (schema foundation)#3geojaz wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a typed schema for verification specifications by defining the VerificationEntry model and execution Mode literals, updating the Task schema to parse verification_spec as a list of VerificationEntry objects, and adding support for weighting verification results. The review feedback highlights several robust improvements: adding defensive checks when dumping verification_spec entries to avoid crashes with raw dictionaries, wrapping placeholder resolution in a try-except block to handle substitution failures gracefully, logging warnings for duplicate verification entry names to prevent silent overwrites, and ensuring that custom leaf verifiers properly propagate their weight to VerificationResult instances.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| "verification_spec": ( | ||
| [e.model_dump() for e in task.verification_spec] | ||
| if task.verification_spec is not None | ||
| else None | ||
| ), |
There was a problem hiding this comment.
If task.verification_spec contains raw dictionaries (e.g., if the task was loaded bypassing Task.from_dict or using a mock/stub in tests), calling e.model_dump() will raise an AttributeError and crash the harness. Use a defensive check to only call model_dump() if the object has that attribute.
| "verification_spec": ( | |
| [e.model_dump() for e in task.verification_spec] | |
| if task.verification_spec is not None | |
| else None | |
| ), | |
| "verification_spec": ( | |
| [ | |
| e.model_dump() if hasattr(e, "model_dump") else e | |
| for e in task.verification_spec | |
| ] | |
| if task.verification_spec is not None | |
| else None | |
| ), |
| for entry in raw: | ||
| spec_resolved = self._resolve_spec_placeholders( | ||
| entry.spec, cluster_name, target_deployment, namespace | ||
| ) | ||
| try: | ||
| mapping[entry.name] = VerificationSpec(spec_resolved) | ||
| except Exception as exc: # noqa: BLE001 - surface every failure | ||
| _log.warning( | ||
| "verification entry %r failed to validate; skipping: %s", | ||
| entry.name, | ||
| exc, | ||
| ) | ||
| errors.append({"name": entry.name, "reason": str(exc)}) |
There was a problem hiding this comment.
Wrap the placeholder resolution (_resolve_spec_placeholders) inside the try-except block as well. If placeholder substitution fails or raises an unexpected exception, it will currently crash the entire harness run instead of being gracefully caught and logged as a verification parse error.
| for entry in raw: | |
| spec_resolved = self._resolve_spec_placeholders( | |
| entry.spec, cluster_name, target_deployment, namespace | |
| ) | |
| try: | |
| mapping[entry.name] = VerificationSpec(spec_resolved) | |
| except Exception as exc: # noqa: BLE001 - surface every failure | |
| _log.warning( | |
| "verification entry %r failed to validate; skipping: %s", | |
| entry.name, | |
| exc, | |
| ) | |
| errors.append({"name": entry.name, "reason": str(exc)}) | |
| for entry in raw: | |
| try: | |
| spec_resolved = self._resolve_spec_placeholders( | |
| entry.spec, cluster_name, target_deployment, namespace | |
| ) | |
| mapping[entry.name] = VerificationSpec(spec_resolved) | |
| except Exception as exc: # noqa: BLE001 - surface every failure | |
| _log.warning( | |
| "verification entry %r failed to validate; skipping: %s", | |
| entry.name, | |
| exc, | |
| ) | |
| errors.append({"name": entry.name, "reason": str(exc)}) |
| entry.spec, cluster_name, target_deployment, namespace | ||
| ) | ||
| try: | ||
| mapping[entry.name] = VerificationSpec(spec_resolved) |
There was a problem hiding this comment.
If multiple verification entries share the same name, the subsequent entries will silently overwrite the previous ones in mapping. This is likely an authoring error (e.g., copy-paste mistake) and can lead to verification checks being silently skipped. Consider logging a warning when a duplicate name is detected.
if entry.name in mapping:
_log.warning(
"Duplicate verification entry name %r detected; overwriting previous entry.",
entry.name,
)
mapping[entry.name] = VerificationSpec(spec_resolved)|
|
||
| name: str | None = None | ||
| kubeconfig: str | None = None | ||
| weight: float = 1.0 |
There was a problem hiding this comment.
Since VerificationResult defaults weight to 1.0, any custom leaf verifier that overrides verify and instantiates VerificationResult directly (instead of using _poll_to_result) must explicitly forward self.weight to the result (i.e., VerificationResult(..., weight=self.weight)). Otherwise, the configured weight on the verifier will be lost in the result tree. Consider documenting this requirement or ensuring that the runner/agent automatically overrides the result's weight with the verifier's weight during evaluation.
Introduce the models package foundation: the LLMClient interface, the MODELS registry with its get_model factory (provider resolution via core.model_providers), and run_tool_loop, the shared agentic tool-call loop that drives an LLMClient against a set of tools. The loop tests are async, so pytest-asyncio joins the dev dependency group. Signed-off-by: Richard Huang <richackard@gmail.com> GitOrigin-RevId: a78c8b3
Waves 4 and 5 collapse from 25 PRs to 10. Each batch is authored by someone new to the area, with its main past contributors reviewing.
…ion) Give verification_spec a real schema so a task can declare which checks feed the correctness, safety, and catastrophic scoring signals. Today the field is untyped (Any), leaving nowhere to express that distinction. - Add VerificationEntry (name, role, spec) and type Task.verification_spec as list[VerificationEntry] | None. role defaults to correctness, so existing specs stay valid unchanged. - Declare optional mode and weight on BaseVerifier (and weight on VerificationResult); additive metadata, not yet consumed. - Adapt the harness parse path to typed entries, preserving verification_parse_errors, with defensive handling for raw dicts, placeholder-resolution failures, and duplicate entry names. Behavior-neutral: no new verifiers and no scoring logic. optimize-scale parses and runs unchanged.
c1cfa5c to
53d3a2f
Compare
Align the verification_spec role vocabulary with the task/verifier schema doc: roles are objective | safeguard, and a safeguard carries an explicit severity (recoverable | catastrophic) that decides how a violation scores. - Role: correctness|safety|catastrophic -> objective|safeguard. role still defaults to objective, so existing specs stay valid unchanged. - Add Severity (recoverable|catastrophic) and a severity field on VerificationEntry, enforced by a validator: required when role='safeguard', forbidden when role='objective'. - Update the BaseVerifier.mode role->mode mapping docstring accordingly. - Update entry-schema and task-schema tests for the new vocabulary and add severity coverage. Behavior-neutral: still schema only, no new verifiers and no scoring logic (Part 1 of 2).
Address review: a custom verifier that overrides verify() and builds a VerificationResult directly (bypassing _poll_to_result) would silently drop its configured weight, leaving the default 1.0 and corrupting the rollup. Make the runner the single source of truth: _run_leaf re-stamps the leaf's configured weight onto the result after verify() returns, so no verify() implementation has to remember to forward it. Nodes without a weight attribute (e.g. compounds) are left untouched. - runner._run_leaf: apply node.weight to the result post-verify. - base.py: update the VerificationResult.weight docstring (runner is now authoritative; direct-build verifiers need not forward weight). - tests: assert the runner stamps weight even when verify() omits it, and preserves the default 1.0 when unset.
Type
verification_specwith roles (schema foundation)Where this comes from
We've been working through how to implement the v1.0 scoring framework (the
cat_v * c^0.5 * rec_v^0.5outcome score). Computing those three signals means a task has to declare which checks are objectives (feed correctnessc) and which are safeguards (feedrec_v/cat_vdepending on severity). Todayverification_specis untyped (Any), so there is nowhere to put that distinction. This PR is the foundational piece: give the field a real schema with aroleandseverity, so the scoring layer has something to roll up. It is intentionally split from the machinery that consumes it so this part can land as a small, behavior-neutral change.Vocabulary follows the task/verifier schema doc: roles are
objective | safeguard, and a safeguard carriesseverity: recoverable | catastrophic.Part 1 of 2.
What this does
VerificationEntry(name,role,severity,spec) and typesTask.verification_specaslist[VerificationEntry] | None.roleis one ofobjective | safeguardand defaults toobjective, so every existing spec stays valid unchanged.severity(recoverable | catastrophic) is required whenrole: safeguardand forbidden whenrole: objective, enforced by a model validator.modeandweightfields onBaseVerifier(andweightonVerificationResult). Additive metadata; nothing consumes them for scoring in this PR.VerifierAgent._run_leaf) is authoritative for a leaf'sweight: it stamps the verifier's configured weight onto the result afterverify()returns, so a custom verifier that builds aVerificationResultdirectly (bypassing_poll_to_result) can never silently drop it.verification_parse_errorsbehavior.What this does NOT do
No new verifiers, no scoring logic, no runtime behavior change.
tasks/common/optimize-scaleparses and runs exactly as before (regression test included). The verifier vocabulary and thec/rec_v/cat_vrollup that consumerole/severity/mode/weightland in Part 2.