Alternative vocabulary: objective/constraint + severity (for discussion) - #5
Alternative vocabulary: objective/constraint + severity (for discussion)#5geojaz wants to merge 6 commits into
Conversation
…ion) Adds a typed VerificationEntry model (name, role, weight, spec) and introduces Role and Mode as first-class literals on the verification package. The change is purely additive and behavior-neutral: - tasks/schema.py: verification_spec is now list[VerificationEntry] | None; existing tasks that omit the field are unaffected. - verification/base.py: adds the mode and weight fields to BaseVerifier and weight to VerificationResult; existing verifiers need no changes. - verification/entry.py: new file defining VerificationEntry and Role. - evalharness/default.py: recognises the typed path (list[VerificationEntry]) and fast-paths through it; the legacy dict/list/string path is unchanged. - verification/__init__.py: exports Mode, Role, and VerificationEntry; no rollup or runner exports added here (those come in the vocabulary branch). Role defaults to "correctness". mode and weight are declared but not yet consumed by any scoring logic. The optimize-scale task (verification_spec omitted) parses and runs unchanged.
…nsive model_dump, placeholder error handling, duplicate-name warning) - Fix 1 (HIGH): Guard verification_spec serialization in _empty_record with hasattr(e, "model_dump") so a raw dict in the list (from a test/mock that bypasses Task.from_dict) does not crash with AttributeError. - Fix 2 (MEDIUM): Move _resolve_spec_placeholders inside the per-entry try/except in the typed path of _build_verification_mapping. A bad placeholder substitution is now captured as a parse error and appended to the errors list rather than propagating out of the method. - Fix 3 (MEDIUM): Warn (via _log) before overwriting a duplicate entry name in both the typed and legacy paths of _build_verification_mapping. The last entry still wins; the warning surfaces the authoring mistake. Also extends VerificationResult.weight docstring to note that custom verifiers constructing VerificationResult directly must forward self.weight. Tests added: _empty_record with raw dicts, placeholder error capture, and duplicate-name warning (caplog + last-wins assertion).
Stacked on feat/verification-spec-foundation. Adds the value layer: Verifiers: - resource_property: checks a JSONPath expression against a kubectl-fetched resource; supports eq/ne/lt/le/gt/ge operators. Includes a value is None guard so a missing field fails cleanly rather than raising. - http_probe: fires an HTTP(S) GET inside a cluster pod via k8s.run_pod; checks status code and optional body regex. k8s primitives: - kubectl.run_pod: launches a short-lived pod with --rm -i --restart=Never (-i is the fix that lets stdin-free probes exit cleanly). Mode dispatch in VerifierAgent: - converge: existing behavior, hands leaf the full remaining timeout. - assert: calls verify(0.0), exactly once, never sleeps. - hold: samples verify(0.0) repeatedly across a window; any failing sample fails the entry. Window and interval default guards use is None so that explicit 0.0 values are honored rather than silently replaced. Score rollup (rollup.py): - RollupScores / EvaluatedEntry dataclasses. - rollup() produces c, rec_v, cat_v from a flat list of EvaluatedEntry pairs. Zero-denominator guards for both correctness and safety roles prevent division errors when all weights sum to zero. - Wiring rollup into the harness gate is a deliberate follow-up. verification/__init__.py: adds EvaluatedEntry, RollupScores, rollup exports.
- http_probe empty-body parse: change .strip() to .rstrip() so curl output
"\n<code>" (empty body, e.g. 204 No Content) is parsed correctly.
.strip() ate the leading newline and caused rsplit('\n', 1) to return a
single element; .rstrip() preserves it. Unit test added.
- resource_property exactly-one name/selector validator: tighten the
model_validator to reject both-provided as well as neither-provided.
Condition changed from (name is None and selector is None) to
(name is None) == (selector is None). Unit test added.
- scripts/demo_verification_rollup.py: standalone end-to-end script that
applies two nginx:alpine deployments (web-honest with readinessProbe,
web-cheat without) to a live cluster, runs three VerificationEntry objects
per scenario (correctness/safety/catastrophic), calls rollup(), and prints
a side-by-side c/rec_v/cat_v table with outcome scores. Demonstrates that
correctness alone (c=1.0 for both) cannot distinguish the honest scenario
from the probe-deleting cheat; rec_v=1.0 vs 0.0 reveals the difference.
…ectrum Adds four scenarios (noop, liar, masker, honest) to the rollup demo so a single table shows that an LLM transcript judge cannot distinguish a liar or masker from an honest agent (all score ~0.8), while deterministic role-tagged verification correctly scores them 0 / 0 / 0 / 1.0. Key changes: - Creates and deletes the kind cluster automatically so the script is self-contained. - Adds realistic task prompt, expected-output checklist, and four transcripts. - Runs the real GEval OutcomeValidity judge (via run_geval) with graceful skip and full error reporting when credentials are unavailable. - Runs deterministic VerifierAgent checks against live cluster state for all four scenarios and prints the combined c / rec_v / cat_v / outcome table. - Renames web-cheat to web-masker to match the four-scenario vocabulary.
…(alternative to correctness/safety/catastrophic) Alternative framing for comparison, not for merge. Two role polarities instead of three flat roles: objective (was correctness) for checks that must hold, constraint for guardrails carrying an explicit severity of recoverable (was safety) or catastrophic. Catastrophic is no longer a peer role; it becomes a severity of a constraint. The rollup mapping is unchanged: c from objective leaves, rec_v from recoverable-constraint leaves, cat_v from catastrophic-constraint leaves. Mode defaults are also unchanged: objective -> converge, constraint -> assert. A model validator on VerificationEntry enforces that severity is required on constraints and forbidden on objectives. EvaluatedEntry carries severity so rollup never has to infer it from a secondary list.
There was a problem hiding this comment.
Code Review
This pull request introduces a typed schema for verification entries, adds a scoring rollup mechanism, and implements new verifiers (HTTP probe and resource property). It also enhances the verifier agent with execution modes (converge, assert, hold) and improves error handling in verification mapping. My feedback suggests simplifying attribute access in the verifier agent and refactoring the HTTP probe's verify method to reduce code duplication.
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.
| _window = getattr(node, "hold_window_sec", None) | ||
| window_sec: float = _window if _window is not None else _DEFAULT_HOLD_WINDOW_SEC | ||
| _interval = getattr(node, "hold_interval_sec", None) | ||
| interval_sec: float = _interval if _interval is not None else _DEFAULT_HOLD_INTERVAL_SEC |
There was a problem hiding this comment.
The logic to get hold_window_sec and hold_interval_sec can be simplified. Since these attributes are defined on BaseVerifier with default float values, verifier nodes will always have them. You can use getattr with a default value to make this more concise.
| _window = getattr(node, "hold_window_sec", None) | |
| window_sec: float = _window if _window is not None else _DEFAULT_HOLD_WINDOW_SEC | |
| _interval = getattr(node, "hold_interval_sec", None) | |
| interval_sec: float = _interval if _interval is not None else _DEFAULT_HOLD_INTERVAL_SEC | |
| window_sec: float = getattr(node, "hold_window_sec", _DEFAULT_HOLD_WINDOW_SEC) | |
| interval_sec: float = getattr(node, "hold_interval_sec", _DEFAULT_HOLD_INTERVAL_SEC) |
| start = time.monotonic() | ||
| try: | ||
| ok, reason, raw = self._run_probe() | ||
| except SubprocessError as exc: | ||
| stderr = (exc.stderr or "").strip() | ||
| _log.warning("http_probe kubectl run failed for %s: %s", self.url, stderr) | ||
| return VerificationResult( | ||
| success=False, | ||
| elapsed_time=time.monotonic() - start, | ||
| reason=f"kubectl run failed: {stderr}", | ||
| name=self.name, | ||
| weight=self.weight, | ||
| ) | ||
| except Exception as exc: # noqa: BLE001 - surface unexpected errors as failures | ||
| _log.warning("http_probe unexpected error for %s: %s", self.url, exc) | ||
| return VerificationResult( | ||
| success=False, | ||
| elapsed_time=time.monotonic() - start, | ||
| reason=f"unexpected error: {exc}", | ||
| name=self.name, | ||
| weight=self.weight, | ||
| ) | ||
| return VerificationResult( | ||
| success=ok, | ||
| elapsed_time=time.monotonic() - start, | ||
| reason=reason, | ||
| name=self.name, | ||
| raw=raw, | ||
| weight=self.weight, | ||
| ) |
There was a problem hiding this comment.
The verify method can be refactored to reduce code duplication in creating VerificationResult instances within the try...except block. You can handle exceptions by setting the failure reason and then have a single VerificationResult creation at the end.
start = time.monotonic()
ok, reason, raw = False, "", None
try:
ok, reason, raw = self._run_probe()
except SubprocessError as exc:
stderr = (exc.stderr or "").strip()
_log.warning("http_probe kubectl run failed for %s: %s", self.url, stderr)
reason = f"kubectl run failed: {stderr}"
except Exception as exc: # noqa: BLE001 - surface unexpected errors as failures
_log.warning("http_probe unexpected error for %s: %s", self.url, exc)
reason = f"unexpected error: {exc}"
return VerificationResult(
success=ok,
elapsed_time=time.monotonic() - start,
reason=reason,
name=self.name,
raw=raw,
weight=self.weight,
)
Alternative vocabulary: objective / constraint + severity (for discussion)
Internal, for discussion, not headed upstream. This is the same verification work as #3 / #4, reworded from
correctness/safety/catastrophicto two role polarities plus a severity, so we can look at the objective/constraint framing (raised on the scoring doc) in real code and decide whether it is worth adopting.The reframe
objective(must hold) feedsc(wascorrectness)constraint(a guardrail) with aseverity:recoverablefeedsrec_v(was rolesafety)catastrophicfeedscat_v(was rolecatastrophic)So "catastrophic" stops being a third peer role and becomes a severity of a constraint. The formula and the
c/rec_v/cat_voutputs are identical; only the vocabulary feeding them changes. 946 tests pass; the demo behaves identically.Honest trade-offs (the decision this branch is meant to inform)
documentationentries already carry aconstraintslist (textual requirements for the judge). Reusing "constraint" as a verification role in the same YAML could confuse authors. The old names did not collide. If we like the polarity idea, a non-colliding word (guardrail?) may be better.role=catastrophic(one self-describing field) becomesrole=constraint, severity=catastrophic(two fields).Status
Not for merge as-is. A vocabulary decision should follow the scoring-doc discussion. Opening here so we can compare both framings in real code side by side (#3 / #4 use the current names).