Skip to content

feat: fixture-based evaluation harness - #65

Open
Benkapner wants to merge 3 commits into
mainfrom
feat/evals
Open

feat: fixture-based evaluation harness#65
Benkapner wants to merge 3 commits into
mainfrom
feat/evals

Conversation

@Benkapner

Copy link
Copy Markdown
Collaborator

Summary

Adds an eval harness so prompt changes can be measured against real model output. No src/ changes.

  • Eval runner (evals/run.py): discovers fixture cases, runs them against a configured LLM endpoint, checks assertions (not golden text), emits a pass/fail table
  • 4 seed fixtures: issue-52 section deletion, short-doc legitimate edit, no-update-needed, CLI reference update
  • CI workflow (eval.yml): workflow_dispatch only (costs real tokens), uploads results as an artifact

Why

The project lives or dies on prompt quality and currently has no way to know whether a prompt change helped. Every test is mocked. This harness makes quality measurable before any further prompt or generation changes land.

Test plan

  • uv run python evals/run.py --case issue-52-deletion --verbose runs against a real endpoint
  • uv run pytest -v passes (415 tests, no changes to existing code)
  • Eval workflow triggers manually in the Actions UI

The project has no way to know whether a prompt change helped. All
tests are mocked and verify control flow, not output quality. Add a
fixture-based eval harness that runs cases against a real LLM endpoint
and checks assertions (file selected, heading preserved, keyword
present) rather than comparing golden text.
Four cases covering the most important quality scenarios:
- issue-52-deletion: model must add refresh_token docs without deleting
  the Rate Limiting, Error Handling, or Troubleshooting sections
- short-doc-legitimate-edit: 10-line config doc gets a new setting;
  must not trip the preservation heuristic
- no-update-needed: internal cache refactor with an unrelated API
  reference doc; correct answer is NO_UPDATE_NEEDED
- cli-reference-update: new CLI flag must appear in the reference page
workflow_dispatch only, never on every PR. Evals cost real tokens and
need model credentials. Supports running a single case or the full
suite. Results are uploaded as a workflow artifact.
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 17, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:27 AM UTC · Completed 5:44 AM UTC

Commit: bc12213 · View workflow run →

@Benkapner Benkapner self-assigned this Aug 17, 2026
@Benkapner
Benkapner requested a review from csoceanu August 17, 2026 05:36
@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [api-contract] evals/run.py:97 — The call to ask_ai_for_updated_content() passes skip_verification=True, but the function signature in src/generation.py does not accept this parameter. This will raise TypeError at runtime, causing every eval case to fail.
    Remediation: Remove the skip_verification=True argument, or add the parameter to ask_ai_for_updated_content().

  • [protected-path] .github/workflows/eval.yml — This PR adds a file under .github/, which is a protected path requiring human approval. No linked issue provides authorization context for this change.

Medium

  • [missing-dependency] pyproject.tomlevals/run.py imports yaml (PyYAML), but pyyaml is not listed in the project's dependencies or dev optional-dependencies. uv sync --extra dev will not install it.
    Remediation: Add pyyaml to the dev optional-dependencies.

  • [silent-false-negative] evals/run.py:107 — The selected assertion only checks files present in results. If a file is listed in selected but doesn't exist in before/, the check silently passes — a typo in expectations.yaml would produce a false pass.
    Remediation: Validate that each file in selected/not_selected exists in doc_files.

  • [command-injection] .github/workflows/eval.yml:33${{ inputs.case }} is interpolated directly into a shell run: block. Although workflow_dispatch limits the attack surface to users with write access, this is a defense-in-depth concern.
    Remediation: Pass via environment variable (EVAL_CASE: ${{ inputs.case }}) and reference $EVAL_CASE in the script.

  • [missing-doc] README.md — Development section does not mention the new evaluation harness. The harness is documented in evals/README.md but not cross-referenced.

  • [missing-doc] CLAUDE.md — Development section mentions pytest but not the evaluation harness.

Low

  • [dead-code] evals/run.py:81total_tokens is initialized to 0, returned, but never incremented.
  • [edge-case] evals/run.py:83expect_no_update: true with empty doc_files passes trivially without verification.
  • [import-organization] evals/run.py:19sys.path.insert between stdlib and third-party imports breaks import grouping.
  • [cli-argument-patterns] evals/run.py:158--verbose argument is missing help text.
  • [error-messaging] evals/run.py:40 — Print messages use leading spaces inconsistent with codebase convention.

Labels: PR adds a fixture-based evaluation harness (Python) with a new GitHub Actions workflow


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

Comment thread evals/run.py
print(f" {file_path}: {elapsed:.1f}s")

is_no_update = updated.strip() == "NO_UPDATE_NEEDED"
results[file_path] = {"updated": updated, "no_update": is_no_update}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] api-contract

The call to ask_ai_for_updated_content() passes skip_verification=True, but the function signature in src/generation.py does not accept this parameter. This will raise TypeError at runtime, causing every eval case to fail.

Suggested fix: Remove the skip_verification=True argument, or add the parameter to ask_ai_for_updated_content() in src/generation.py.

Comment thread evals/run.py
if f in results and not results[f]["no_update"]:
failures.append(f"Expected {f} to return NO_UPDATE_NEEDED, but it was updated")

if expect_no_update:

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] silent-false-negative

The selected assertion only checks files present in results. If a file is listed in selected but does not exist in before/, the check silently passes. A typo in expectations.yaml would produce a false pass.

Suggested fix: Add a check that each file in selected and not_selected exists in case['doc_files'].

if [ -n "${{ inputs.case }}" ]; then
args="$args --case ${{ inputs.case }}"
fi
uv run python evals/run.py $args

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] command-injection

The workflow_dispatch input inputs.case is interpolated directly into a shell run: block. Although workflow_dispatch requires repo write access (limiting the attack surface), this is a defense-in-depth concern.

Suggested fix: Pass the input via an environment variable (EVAL_CASE: ${{ inputs.case }}) and reference $EVAL_CASE in the script.

Comment thread evals/run.py
expect_no_update = expectations.get("expect_no_update", False)

results = {}
for file_path, content in case["doc_files"].items():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] dead-code

total_tokens is initialized to 0, returned, but never incremented anywhere in run_case().

Comment thread evals/run.py
results = {}
for file_path, content in case["doc_files"].items():
if verbose:
print(f" Generating update for {file_path}...")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

When expect_no_update is True and doc_files is empty, the loop iterates over nothing and the case passes trivially without verification.

Comment thread evals/run.py

import yaml

# Add src/ to path so we can import the action's modules

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] import-organization

sys.path.insert between stdlib and third-party imports breaks import grouping convention.

Comment thread evals/run.py
case = load_case(case_dir)
print(f" Running: {case['name']}...")
try:
passed, failures, tokens = run_case(case, verbose=args.verbose)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] cli-argument-patterns

--verbose argument is missing help text.

Comment thread evals/run.py
return cases


def load_case(case_dir):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] error-messaging

Print messages use leading spaces inconsistent with codebase convention.

@fullsend-ai-review fullsend-ai-review Bot added feature python Pull requests that update python code github_actions Pull requests that update GitHub Actions code labels Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature github_actions Pull requests that update GitHub Actions code python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant