Skip to content
Back to skills

1127 Verifier Fusion Plan B9054e2b

ASecurity

This document covers **two related refactors**: 1. **Part 1**: Verifier fusion for PTCG gameplay evals (task reward + zero-shot verifier + fused reward) 2. **Part 2**: Rubrics consolidation (delete `TaskInfo.rubric`, single source via `/info`) Both changes tie together because they clean up how rubrics flow from task apps to the backend verifier. ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
testingtypescriptpythonrustgoreactexpressfastapiapibackendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1127-verifier_fusion_plan_b9054e2b --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1127 Verifier Fusion Plan B9054e2b?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1127 Verifier Fusion Plan B9054e2b
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1127-verifier-fusion-plan-b9054e2b/badge)](https://www.skillsdirectory.com/skills/tools-only-1127-verifier-fusion-plan-b9054e2b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Verifier Fusion Plan: PTCG Gameplay Eval + Rubrics Consolidation Refactor

This document covers **two related refactors**:

1. **Part 1**: Verifier fusion for PTCG gameplay evals (task reward + zero-shot verifier + fused reward)
2. **Part 2**: Rubrics consolidation (delete `TaskInfo.rubric`, single source via `/info`)

Both changes tie together because they clean up how rubrics flow from task apps to the backend verifier.

---

## Part 1: Verifier Fusion for PTCG Gameplay Eval

### Goal

Run **headless gameplay evals** for `demos/gepa_ptcg` where:

- The **task app** computes the canonical task reward (win/loss or outcome reward).
- The Synth backend runs a **zero-shot verifier** that evaluates **gameplay quality** from the hydrated trace against a rubric.
- The backend **fuses** task reward + verifier reward into a single per-seed `reward`.

This is intended to work with the existing **eval job** flow (not prompt-learning/GEPA optimization yet).

---

### Goal / Non-goals

- **Goal**: In eval jobs, record *both*:
  - `outcome_reward` (task app, who won)
  - `verifier_reward` (zero-shot rubric, gameplay quality)
  - `reward` (fused)
- **Goal**: Reuse **interceptor hydration** for traces (avoid building v3 traces in task app).
- **Non-goal**: Modify `monorepo/specs` (explicitly forbidden).
- **Non-goal**: Embed LLM calls into Rust servers / UI harness. This is headless LocalAPI.

---

### Current state (what already exists)

#### 1) Eval job pipeline supports verifier evaluation

The backend eval job service supports an optional `verifier_config` in the eval request.
If present and enabled, it computes a per-seed `verifier_reward` and then fuses it into the final `reward`.

#### 2) Verifier endpoint exists and is zero-shot capable

Backend route: `POST /api/graphs/verifiers/completions`

It supports built-in zero-shot verifier graph IDs:
- `zero_shot_verifier` (auto routing)
- `zero_shot_verifier_rubric_single`, `..._mapreduce`, `..._rlm`, etc.

#### 3) Trace hydration exists for eval jobs

Eval jobs already hydrate v3 traces from the interceptor store and normalize rollouts to v3 traces for verifier evaluation.
This means task apps do **not** need to build tracing-v3 `event_history` manually, as long as:
- LLM calls go through the interceptor (which we already do in `gepa_ptcg`).

---

### Proposed reward semantics

We want:

- **Task reward** (from task app / LocalAPI): \( r_{local} \in [0, 1] \)
  - Example: win=1, loss=0, draw=0.5, etc.
- **Verifier reward** (from backend verifier): \( r_{verifier} \in [0, 1] \)
  - A rubric-based "gameplay quality" reward independent of win/loss
- **Fused reward**:

\[
r_{final} = w_{local}\cdot r_{local} + w_{verifier}\cdot r_{verifier}
\]

Example weights:
- `w_env=0.5`, `w_verifier=0.5` (simple balanced)

---

### Reward semantics: double-counting risk (CRITICAL)

#### The problem

The verifier pipeline can optionally accept the task app reward as input (we will call it `local_api_reward`):

```python
# backend/app/routes/eval/scoring.py (current)
verifier_result = await verifier.reward_trajectory(
    ...,
    local_api_reward=local_api_rewards.get(seed),  # task app reward passed in
)
```

Depending on the verifier graph implementation, the verifier may:
- **Ignore** `local_api_reward` and evaluate purely on trace + rubric (desired for independent fusion)
- **Incorporate** `local_api_reward` into its output (causes double-counting when fused again)

If the verifier incorporates `env_reward`, and then the eval job service fuses:

```
reward = w_local * local_api_reward + w_verifier * verifier_reward
```

…the task reward gets counted **twice**: once inside `verifier_reward`, once in the outer fusion.

#### The fix (required monorepo change)

To guarantee no double-counting, we change the eval verifier evaluation call to:

```python
# backend/app/routes/eval/scoring.py (proposed)
verifier_result = await verifier.reward_trajectory(
    ...,
    local_api_reward=None,  # <-- do NOT pass task reward into verifier
)
```

This ensures the verifier evaluates only the trace + rubric, and fusion happens exactly once in the eval job service.

#### Code change

Single line change in `backend/app/routes/eval/scoring.py`:

```diff
- local_api_reward=local_api_rewards.get(seed),
+ local_api_reward=None,
```

---

### Gameplay-quality rubric (initial draft)

We want a generic rubric that works for "agent plays a turn-based game":

- **Event criteria** (local action quality):
  - legality / prompt following (choose only allowed actions)
  - progress / avoid stalling
  - attack / advance board when beneficial
  - resource management (energy attach once per turn, don't waste)
- **Outcome criteria** (global quality):
  - win or create advantage
  - avoid obvious blunders

These criteria must be expressed in the backend's expected rubric format:
- `rubric.event`: list of dict criteria
- `rubric.outcome`: list of dict criteria

---

### Verifier configuration (eval request)

We will pass `verifier_config` in the eval job request (SDK: `EvalJobConfig.verifier_config`).

Example:

```json
{
  "enabled": true,
  "reward_source": "fused",
  "backend_base": "http://localhost:8000",
  "backend_api_key": "<SYNTH_API_KEY>",
  "verifier_graph_id": "zero_shot_verifier_rubric_single",
  "backend_provider": "openai",
  "backend_model": "gpt-4.1-mini",
  "backend_event_enabled": true,
  "backend_outcome_enabled": true,
  "concurrency": 4,
  "weight_env": 0.5,
  "weight_event": 0.0,
  "weight_outcome": 0.5
}
```

Notes:
- We fuse only outcome-level verifier reward into final reward initially (keep it simple).
- If event-level evaluation returns per-event totals, we can later add `weight_event > 0`.

---

### Implementation steps (Part 1)

#### A) Task app: expose a gameplay-quality rubric

- Add a `RubricBundle` to `demos/gepa_ptcg/localapi_ptcg.py` via `LocalAPIConfig(rubrics=...)`
- Ensure `/info` returns `rubrics` for the backend to use.

#### B) Eval runner: pass verifier_config

- Update `demos/gepa_ptcg/run_demo.py` to populate `EvalJobConfig.verifier_config` with:
  - `verifier_graph_id="zero_shot_verifier_rubric_single"` (or `zero_shot_verifier`)
  - `reward_source="fused"`
  - weights as above

#### C) Backend: enforce independence / prevent double-counting

- Make the single-line change in `backend/app/routes/eval/scoring.py` (`env_reward=None`).

---

### Validation / acceptance criteria (Part 1)

For a small local run (e.g. 5 seeds) we should see:
- Per seed result row contains:
  - `local_api_reward` (task app)
  - `verifier_reward` (non-null for most seeds)
  - `reward` differs from both and matches the configured fusion weights
- Backend logs show:
  - trace hydration succeeded
  - verifier endpoint calls succeeded (200)

Failure modes and what they mean:
- **verifier_reward is null**: rubric missing/unparseable, verifier_graph_id wrong, trace hydration missing, or verifier endpoint errors.
- **verifier_reward correlates too strongly with win**: likely env_reward leaked into verifier evaluation or rubric is too outcome-focused.

Note: this doc uses `local_api_reward` / `verifier_reward` naming. Existing persisted fields in the backend may still be
named `outcome_reward` / `verifier_reward` until the backend refactor lands; the intent is:
- `outcome_reward` == `local_api_reward`
- `verifier_reward` == `verifier_reward` (canonical naming)

---

### Open questions (Part 1)

1) **Verifier graph id**: confirm which one to use:
   - `zero_shot_verifier_rubric_single` (fast)
   - `zero_shot_verifier_rubric_mapreduce` (slower, potentially higher quality)
   - `zero_shot_verifier` (auto routes)
2) **Judge model** for the verifier:
   - keep cheap (`gpt-4.1-nano` / `gpt-5-nano`) vs better (`gpt-4.1-mini`)
3) **Fusion weights**:
   - start 50/50 or bias toward win (e.g. 0.7 env / 0.3 verifier)?

---

## Part 2: Rubrics Consolidation Refactor (Breaking Change)

### Background

The SDK currently has **two ways** for task apps to advertise rubrics:

| Path | Location | Status | How backend consumes |
|------|----------|--------|---------------------|
| **Legacy** | `TaskInfo.rubric` (returned per-seed from `/task_info?seed=...`) | `[DEPRECATED]` in contracts | Backend fetches `/task_info`, extracts `.rubric`, normalizes into verifier payload |
| **Modern** | `LocalAPIConfig.rubrics` (exposed via `GET /info` as `rubrics.outcome` / `rubrics.events`) | Canonical, preferred | Backend fetches `/info`, reads `rubrics` bundle, uses directly |

The legacy path exists because early task apps populated `TaskInfo.rubric`. The modern path was added to decouple per-instance metadata from global rubric definitions.

**Problem**: Having two sources creates:
- Maintenance burden (backend must merge/fallback)
- Confusion (which one is authoritative?)
- Fragile normalization code (handles lists, dicts, Pydantic models, etc.)

### Decision: Single Source via `/info`

**Canonical source**: `GET /info` → `rubrics` field (a `RubricBundle` with `.outcome` and `.events`).

**Remove**: `TaskInfo.rubric` field entirely from the SDK contracts and backend consumption logic.

**No grace period. No fallback. The deprecated field has been marked long enough — time to rip off the bandaid.**

---

### Canonical rubric data model (synth-ai SDK)

The rubric data model lives in `synth_ai.sdk.task.rubrics`:

```python
# synth_ai/sdk/task/rubrics/models.py

class Criterion(BaseModel):
    id: str                    # unique criterion identifier
    description: str           # what this criterion evaluates
    weight: float = 1.0        # relative importance (must be > 0)
    required: bool = False     # if True, failing this criterion fails the rubric

class Rubric(BaseModel):
    version: str = "1.0"
    goal_text: str | None = None  # high-level goal description (optional)
    criteria: list[Criterion] = []  # list of evaluation criteria
    aggregation: str = "weighted_sum"  # how to combine criterion rewards (options: "sum", "weighted_sum", "custom", "inherit")

# synth_ai/sdk/task/server.py

class RubricBundle(BaseModel):  # MUST be BaseModel (not @dataclass) for OpenAPI compatibility
    """Rubric bundle exposed via /info endpoint. Must be Pydantic BaseModel for OpenAPI schema generation."""
    outcome: Rubric | None = None   # outcome-level rubric (end of rollout)
    events: Rubric | None = None    # event-level rubric (per action/step)
```

Task apps expose rubrics via `LocalAPIConfig(rubrics=RubricBundle(...))`, which the SDK serves from `GET /info` as:

```json
{
  "rubrics": {
    "outcome": { "version": "1.0", "goal_text": "...", "criteria": [...] },
    "events": { "version": "1.0", "goal_text": "...", "criteria": [...] }
  }
}
```

#### How to assemble a rubric (complete example)

```python
from synth_ai.sdk.task.rubrics import Criterion, Rubric
from synth_ai.sdk.task.server import LocalAPIConfig, RubricBundle

# Step 1: Define criteria
legal_action_criterion = Criterion(
    id="legal_actions",
    description="Agent only chooses legal actions from available_actions",
    weight=1.0,
    required=True,  # Fail fast on illegal actions
)

strategic_play_criterion = Criterion(
    id="strategic_play",
    description="Agent makes strategic decisions (attacks when beneficial, manages energy)",
    weight=0.8,
    required=False,
)

avoid_stalling_criterion = Criterion(
    id="avoid_stalling",
    description="Agent progresses the game state and avoids infinite loops",
    weight=0.6,
    required=False,
)

# Step 2: Create a Rubric with criteria
gameplay_rubric = Rubric(
    version="1.0",
    goal_text="Evaluate Pokemon TCG gameplay quality",
    criteria=[
        legal_action_criterion,
        strategic_play_criterion,
        avoid_stalling_criterion,
    ],
    aggregation="weighted_sum",  # Options: "sum", "weighted_sum", "custom", "inherit"
)

# Step 3: Create a RubricBundle (can include both outcome and events rubrics)
rubric_bundle = RubricBundle(
    outcome=gameplay_rubric,  # Evaluated at end of rollout
    events=None,  # Optional: per-action rubric for step-wise evaluation
)

# Step 4: Pass to LocalAPIConfig
def build_config() -> LocalAPIConfig:
    return LocalAPIConfig(
        app_id="gepa_ptcg",
        name="Pokemon TCG Gameplay",
        description="Headless Pokemon TCG gameplay evaluation",
        provide_taskset_description=lambda: {"splits": ["train"]},
        provide_task_instances=lambda seeds: [...],
        rollout=run_rollout,
        rubrics=rubric_bundle,  # <-- Rubrics go here
    )
```

**Notes:**
- **`Criterion`**: Each criterion has an `id` (unique), `description` (what it evaluates), `weight` (relative importance, must be > 0), and `required` (if True, failing this fails the entire rubric).
- **`Rubric`**: Contains a list of criteria, optional `goal_text` (high-level description), and `aggregation` method (how to combine criterion rewards).
- **`RubricBundle`**: Contains `outcome` (evaluated at end of rollout) and/or `events` (evaluated per action/step). At least one must be provided.
- **Weights**: Don't need to sum to 1.0 (flexible model). The verifier will normalize as needed.
- **Required criteria**: If `required=True`, failing that criterion causes the entire rubric to fail (useful for "must be legal" checks).

**OpenAPI compatibility (REQUIRED for Rust/non-Python clients):**

All contract/boundary data classes must be OpenAPI-compatible so users can generate clients in Rust, TypeScript, etc.

- ✅ **`Criterion` and `Rubric`**: Already Pydantic `BaseModel` → OpenAPI-compatible
- ❌ **`RubricBundle`**: Currently a `@dataclass` → **MUST be converted to Pydantic `BaseModel`**
- ❌ **`/info` endpoint**: Currently returns `Mapping[str, Any]` → **MUST have typed response model**

**Required changes:**
1. Convert `RubricBundle` from `@dataclass` to Pydantic `BaseModel` (enables automatic OpenAPI schema generation)
2. Create `InfoResponse` Pydantic model for `/info` endpoint with typed `rubrics` field
3. Update `/info` endpoint to use `response_model=InfoResponse`

This ensures FastAPI generates complete OpenAPI schema that Rust/TypeScript clients can consume.

---

### Scope of changes

#### Inventory (audit of current references)

This is an explicit audit so we don’t miss any callers during the refactor. As of 2026-01-13:

**synth-ai** (task-app side)
- `demos/gepa_ptcg/localapi_ptcg.py`: currently constructs `RubricInfo` and attaches it to `TaskInfo(rubric=...)`.
- `demos/gepa_crafter_vlm/demo_crafter_react.py`: sets `TaskInfo(..., rubric={...})` (legacy pattern).
- `demos/web-design/web_design_task_app.py`: uses `RubricInfo` in `TaskInfo(rubric=RubricInfo(...))` (line 239).
- `demos/web-design/run_demo.py`: imports and uses `RubricInfo`, `RubricCriterion`, `RubricSection` (lines 54, 57, 567).
- `synth_ai/sdk/task/contracts.py`: defines `RubricInfo` and `TaskInfo.rubric` (deprecated).
- `synth_ai/sdk/task/__init__.py`: re-exports `RubricInfo` (and related legacy symbols like `RubricCriterion`, `RubricSection`).
- `synth_ai/sdk/task/localapi_template.py`: template yields `TaskInfo(...)` (ensure no rubric fields).
- `synth_ai/cli/lib/apps/task_app.py`: validates `/info` and `/task_info` and already understands `/info.rubrics`.

**monorepo** (backend side)
- `backend/app/routes/eval/job_service.py`: captures task app reward, stores `outcome_reward`, calls verifier, then fuses into `reward`.
- `backend/app/routes/eval/scoring.py`: currently passes task reward into verifier (`env_reward=...`) (double-counting risk).
- `backend/app/routes/eval/models.py` and `backend/app/routes/eval/routes.py`: expose `outcome_reward` and `verifier_reward`.
- `backend/app/routes/prompt_learning/core/verifying.py`: passes `env_reward` into `RubricPipeline.reward(...)`.
- `backend/app/routes/prompt_learning/core/rubric_pipeline.py`: consumes `task_info["rubric"]` and has fallback merge-from-`/info`; also explicitly computes an env component in the verifier reward.
- `backend/app/routes/prompt_learning/routes_online.py`: builds rubric payload from TaskInfo (`_build_rubric_payload(...)`).
- Prompt-learning optimizers rely on outcome keys:
  - `backend/app/routes/prompt_learning/algorithm/mipro/optimizer/optimizer.py`: prefers `outcome_reward`.
  - `backend/app/routes/prompt_learning/algorithm/gepa/optimizer.py`: reads/writes `verifier_reward` and `outcome_reward`.
- GEPA/GraphGen integration has separate “rubric” concepts (not LocalAPI rubrics):
  - `backend/graphs/gepa_integration/graph_evolve_job.py`: `task_metadata.get("rubric")`, `task.get("rubric")` (GraphGen tasks).
  - `backend/app/routes/graphgen/*`: uses `task.rubric` (GraphGen rubric).

Note: GraphGen’s `task.rubric` is *not* the same contract as LocalAPI `/info.rubrics`. This plan does not unify those
schemas, but we list them so reviewers know what is and isn’t being changed.

#### synth-ai SDK (breaking)

| File | Change |
|------|--------|
| `synth_ai/sdk/task/contracts.py` | **Delete** `RubricInfo` and **delete** `TaskInfo.rubric` entirely (breaking) |
| `synth_ai/sdk/task/__init__.py` | **Remove** legacy exports (`RubricInfo`, anything only supporting `TaskInfo.rubric`) |
| `synth_ai/sdk/task/server.py` | **REQUIRED for OpenAPI**: Convert `RubricBundle` from `@dataclass` to Pydantic `BaseModel`. Create `InfoResponse` Pydantic model for `/info` endpoint with typed `rubrics: RubricBundle | None` field. Update `/info` endpoint to use `response_model=InfoResponse`. This enables OpenAPI schema generation for Rust/TypeScript clients. |
| `synth_ai/sdk/task/localapi_template.py` | Ensure template does **not** set or mention `TaskInfo.rubric` |
| `synth_ai/cli/lib/apps/task_app.py` | Confirm validators treat `/info.rubrics` as canonical and do not require `/task_info` to contain rubric |
| `synth_ai/sdk/graphs/completions.py` | No change required (this is *verifier API input* rubrics, separate from LocalAPI advertising) |
| All `synth-ai/demos/*` task apps | Remove any `TaskInfo(rubric=...)` usage; move to `LocalAPIConfig(rubrics=RubricBundle(...))` |

#### monorepo backend (breaking)

| File | Change |
|------|--------|
| `backend/app/routes/prompt_learning/core/rubric_pipeline.py` | **Delete** all reads of `task_info["rubric"]` and **delete** the fallback merge-from-`/info` logic (because `/info` is now the only source). Also remove/adjust env-component logic so verifier reward can be independent when desired. |
| `backend/app/routes/prompt_learning/core/verifying.py` | Rename `env_reward` plumbing to `local_api_reward` (task app reward) and align internal naming to `verifier_reward`. |
| `backend/app/routes/prompt_learning/routes_online.py` | Remove `_build_rubric_payload(task_info)` path; fetch rubrics from `/info` only. |
| `backend/app/routes/eval/scoring.py` | **Fix double counting**: pass `local_api_reward=None` into verifier evaluation for eval jobs. |
| `backend/app/routes/eval/job_service.py` | Rename internal variables to `local_api_reward` / `verifier_reward` and ensure persisted results store both separately. |
| `backend/app/routes/eval/models.py` | Add canonical fields (or aliases) so API exposes `local_api_reward` and `verifier_reward` clearly. |
| `backend/app/routes/eval/routes.py` | Expose new canonical names in responses; keep legacy aliases if needed (decision below). |
| `backend/app/routes/prompt_learning/algorithm/mipro/optimizer/optimizer.py` | Keep reading task app reward (rename references internally); keep legacy fallback behavior as needed for older task apps. |
| `backend/app/routes/prompt_learning/algorithm/gepa/optimizer.py` | Align field naming for rewards (`verifier_reward`, `local_api_reward` vs `outcome_reward`). |
| `backend/app/routes/clustered_training/core/algorithms/gspo/pipeline_rl/task_info.py` | Remove TaskInfo rubric payload builders (now `/info` only). |
| Backend unit/integration tests | Rewrite tests that assume `TaskInfo.rubric` exists; update fixtures/mocks to serve `/info.rubrics`. |

**Out of scope (but audited):** GraphGen rubric fields in `backend/app/routes/graphgen/*` and `backend/graphs/gepa_integration/*`
are not LocalAPI rubrics and are handled separately.

#### Integration tests / demo task apps

| Location | Change |
|----------|--------|
| `tests/integration/pipeline_rl/` | Ensure mock task apps serve rubrics from `/info`, not `TaskInfo.rubric`. |
| `tests/backend/integration/workflows/rl/math/rl/hendrycks_math_task_app.py` | Remove `rubric=base.rubric` from `TaskInfo` construction (line 391). |
| `agora_single_file.py` | Update `_blend_rubrics(base_info.rubric, ...)` logic (lines 1595, 1679-1681) to fetch from `/info`. |
| `agora_ex/task_app.py` | Remove `rubric=base.rubric` (line 211). |

---

### New backend rubric fetching logic

Replace all `task_info["rubric"]` reads with a single helper:

```python
# backend/app/routes/prompt_learning/core/rubric_fetcher.py (new file)

from typing import Optional, Dict, Any
import httpx

async def fetch_rubric_bundle(
    task_app_url: str,
    headers: Optional[Dict[str, str]] = None,
    timeout: float = 10.0,
) -> Optional[Dict[str, Any]]:
    """Fetch rubric bundle from task app's /info endpoint.
    
    Returns:
        {"outcome": {...}, "events": {...}} or None if not available.
    """
    url = f"{task_app_url.rstrip('/')}/info"
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            response = await client.get(url, headers=headers or {})
        if response.status_code != 200:
            return None
        data = response.json()
        rubrics = data.get("rubrics")
        if not isinstance(rubrics, dict):
            return None
        return {
            "outcome": rubrics.get("outcome"),
            "events": rubrics.get("events"),
        }
    except Exception:
        return None
```

All backend code that currently reads `task_info["rubric"]` should call `fetch_rubric_bundle()` instead.

---

### Migration path (for external task apps)

Since this is a **breaking change**, external task apps that still use `TaskInfo.rubric` will break.

**No grace period. No fallback.** The deprecated field has been marked for long enough — time to rip off the bandaid.

**Migration guide** (to be published with release):

1. Move rubric definitions from `TaskInfo(rubric=...)` to `LocalAPIConfig(rubrics=RubricBundle(...))`.
2. Delete `rubric=` from all `TaskInfo` construction.
3. Verify `GET /info` returns `rubrics.outcome` and/or `rubrics.events`.

**SDK version gate**: Bump SDK major version (e.g. `synth-ai>=2.0.0` requires `/info` rubrics).

---

### Implementation order (single PR/branch)

All changes will be done in **one coordinated PR** across both repos:

**synth-ai SDK + demos** (breaking changes):
- Delete `RubricInfo` and delete `TaskInfo.rubric` from `synth_ai/sdk/task/contracts.py`.
- Remove exports in `synth_ai/sdk/task/__init__.py` (`RubricInfo`, `RubricCriterion`, `RubricSection`).
- Update all affected demos (`demos/gepa_ptcg`, `demos/gepa_crafter_vlm`, `demos/web-design`) to advertise rubrics via
  `LocalAPIConfig(rubrics=RubricBundle(...))` only.
- Update any SDK templates/docs that mention `TaskInfo.rubric`.
- Bump SDK major version.

**monorepo backend** (breaking changes):
- Add `rubric_fetcher.py` helper.
- Replace all `task_info["rubric"]` reads with `fetch_rubric_bundle()` (and remove any TaskInfo-rubric fallback).
- Update eval verifier evaluation to **not** pass task reward into verifier evaluation:
  - In `backend/app/routes/eval/scoring.py` line 50: Change `env_reward=env_rewards.get(seed)` to `env_reward=None` (prevents double counting).
  - **Note**: The parameter name may still be `env_reward` in the verifier API signature; the key is passing `None` for eval jobs.
- **Naming clarification**: The plan uses `local_api_reward` / `verifier_reward` as canonical names, but existing code uses `env_reward` / `outcome_reward` / `verifier_reward`. We can:
  - Keep existing field names in persisted data (`outcome_reward`, `verifier_reward`) for backward compatibility.
  - Optionally add API aliases (`local_api_reward` → `outcome_reward`, `verifier_reward` → `verifier_reward`) if desired.
  - Rename internal variables for clarity where it doesn't break compatibility.
- Update tests and fixtures to use `/info.rubrics`.

**Integration test pass** (same PR):
- Run full pipeline RL / GEPA / eval test suite.
- Fix any remaining `TaskInfo.rubric` assumptions.
- Validate that `local_api_reward` and `verifier_reward` are both present in outputs and fusion is correct.

---

### Validation / acceptance criteria (Part 2)

- [ ] `RubricInfo` does not exist in `synth-ai` SDK; `TaskInfo` has no `rubric` field.
- [ ] No `TaskInfo.rubric` usage exists anywhere in `synth-ai` demos (verified: `gepa_ptcg`, `gepa_crafter_vlm`, `web-design` all migrated).
- [ ] Backend does not read `task_info["rubric"]` anywhere.
- [ ] All rubric-enabled task apps expose rubrics via `GET /info` as `rubrics.outcome` and/or `rubrics.events`.
- [ ] `RubricBundle` is a Pydantic `BaseModel` (not `@dataclass`) for OpenAPI compatibility.
- [ ] `/info` endpoint has typed response model (`InfoResponse`) for OpenAPI schema generation.
- [ ] FastAPI generates complete OpenAPI schema for `/info` endpoint (verifiable via `/docs` or `/openapi.json`).
- [ ] Eval results contain **two distinct values**:
  - `outcome_reward` (task app reward, also known as `local_api_reward` in plan terminology)
  - `verifier_reward` (verifier reward, canonical naming)
- [ ] Fusion uses them exactly once (no double counting): verifier evaluation receives `env_reward=None` for eval jobs.
- [ ] All existing tests pass (with updates).

---

### Risk assessment

| Risk | Mitigation |
|------|------------|
| External task apps break | Clear migration guide; announce in release notes. **No fallback period.** |
| Backend regression | Comprehensive test coverage for rubric fetching. |
| Performance (extra /info call) | Cache `/info` response per task app per job run. |

---

### Timeline estimate

| Step | Effort |
|------|--------|
| Single PR (SDK + backend + tests) | 3-5 days |
| **Total** | ~1 week |

---

## Summary

This plan covers:

1. **Verifier fusion** for PTCG evals — task app computes win/loss, verifier evaluates gameplay quality, backend fuses.
2. **Reward semantics fix** — pass `local_api_reward=None` to verifier evaluation for eval jobs to prevent double-counting.
3. **Rubrics consolidation** — delete `TaskInfo.rubric`, single source via `GET /info` rubrics.

All three changes are related and should be done together as a coordinated SDK + monorepo refactor.

---

## Finalization notes (what is “done” when this plan is approved)

This plan is considered finalized when:
- The above inventory items are either updated or explicitly marked out-of-scope in code review.
- Single PR lands with:
  - SDK: `TaskInfo.rubric` fully removed and demos migrated
  - Backend: verifier evaluation independent of task reward in eval jobs
  - Backend: explicit result separation (`local_api_reward`, `verifier_reward`)
  - Backend: rubrics sourced from `/info` only
  - Tests: end-to-end validation passes

---

## Verification checklist (comprehensive sweep)

**✅ synth-ai SDK changes:**
- [x] `RubricInfo` deletion scoped (`contracts.py`)
- [x] `TaskInfo.rubric` deletion scoped (`contracts.py`)
- [x] Legacy exports removal scoped (`__init__.py`: `RubricInfo`, `RubricCriterion`, `RubricSection`)
- [x] All demos identified: `gepa_ptcg`, `gepa_crafter_vlm`, `web-design`

**✅ monorepo backend changes:**
- [x] Double-counting fix scoped (`eval/scoring.py` line 50: `env_reward=None`)
- [x] Rubric fetching refactor scoped (`rubric_pipeline.py`, `routes_online.py`, `task_info.py`)
- [x] All `task_info["rubric"]` reads identified and scoped for removal
- [x] GraphGen rubrics explicitly marked out-of-scope (with TODOs added)

**✅ Naming clarity:**
- [x] Plan terminology (`local_api_reward`, `verifier_reward`) documented
- [x] Existing field names (`outcome_reward`, `verifier_reward`) acknowledged
- [x] Migration path clarified (keep existing names for compatibility, add aliases if desired)

**✅ 0→1 approach confirmed:**
- [x] No partial deprecation — `TaskInfo.rubric` fully removed
- [x] No grace period — breaking change with migration guide
- [x] Single source of truth — `/info` rubrics only

**✅ Implementation order:**
- [x] Single PR/branch: All changes coordinated together (SDK + backend + tests)

**Plan status: ✅ COMPREHENSIVE AND FINAL**

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.