Skip to content

Alternative vocabulary: objective/constraint + severity (for discussion) - #5

Closed
geojaz wants to merge 6 commits into
mainfrom
experiment/verification-objective-constraint
Closed

Alternative vocabulary: objective/constraint + severity (for discussion)#5
geojaz wants to merge 6 commits into
mainfrom
experiment/verification-objective-constraint

Conversation

@geojaz

@geojaz geojaz commented Jul 16, 2026

Copy link
Copy Markdown
Member

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 / catastrophic to 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) feeds c (was correctness)
  • constraint (a guardrail) with a severity:
    • recoverable feeds rec_v (was role safety)
    • catastrophic feeds cat_v (was role catastrophic)

So "catastrophic" stops being a third peer role and becomes a severity of a constraint. The formula and the c / rec_v / cat_v outputs 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)

  1. "constraint" collides with the existing schema. documentation entries already carry a constraints list (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.
  2. Rename only, not true negation. Every check here is still a must-hold predicate; a "constraint" passes when its guardrail holds. Genuinely expressing "must NOT hold" would need an explicit negate/forbidden option, a separate and larger change. This branch does the rename, not the polarity flip.
  3. Severity is slightly more verbose. role=catastrophic (one self-describing field) becomes role=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).

geojaz added 6 commits July 16, 2026 09:20
…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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +226 to +229
_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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
_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)

Comment on lines +76 to +105
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,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
        )

@geojaz geojaz closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant