Skip to content

Add contextual polytomous LLM judge and IRT response contract - #733

Merged
seonghobae merged 14 commits into
mainfrom
codex/local-llm-judge-irt
Aug 11, 2026
Merged

Add contextual polytomous LLM judge and IRT response contract#733
seonghobae merged 14 commits into
mainfrom
codex/local-llm-judge-irt

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a provider-neutral ContextualOrchestratorJudge that routes every judge call through contextual-orchestrator
  • enforce strict criterion-level JSON, explicit polytomous categories, runtime-derived acceptance, and no keyword/positional repair
  • add LLMJudgeResult.to_irt_row() plus a multi-item dichotomous/polytomous response-matrix validator
  • document the integration and link the contextual-orchestrator ADR/benchmark evidence

Validation

Related

Summary by CodeRabbit

  • New Features

    • Added provider-neutral AI evaluation with configurable criteria, weighted scoring, acceptance thresholds, rationales, and usage details.
    • Added strict structured-response parsing and support for categorical rubrics.
    • Added conversion of evaluation results into dichotomous or polytomous IRT response rows.
    • Added validation for IRT response matrices, including item counts, categories, shape, and allowed values.
    • Exposed evaluation and response-validation capabilities through the public package interface.
  • Documentation

    • Updated the README with guidance for structured evaluations, IRT conversion, and experimental score calibration.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a20ac86c-3d9c-4b34-bfb5-cd3bb8662581

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds a provider-neutral LLM judge, strict structured-result parsing, IRT response validation, public package exports, tests, and README documentation.

Changes

LLM Judge and IRT Integration

Layer / File(s) Summary
IRT response contract
python/fast_mlsirm/irt_contract.py, tests/test_irt_contract.py
Adds validation for dichotomous and polytomous persons-by-items matrices. The contract enforces numeric integer categories, valid bounds, nonempty rows, and at least two items while allowing NaN values.
Structured judge flow
python/fast_mlsirm/llm_judge.py, tests/test_llm_judge.py
Adds immutable criteria and result models, bounded input validation, strict JSON parsing, score and category handling, acceptance derivation, usage tracking, and deterministic IRT row projection.
Public API and workflow documentation
python/fast_mlsirm/__init__.py, README.md
Exports the judge and IRT contract entities and documents structured parsing, IRT conversion, matrix validation, and calibration requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ContextualOrchestratorJudge
  participant ContextualOrchestrator
  participant LLMJudgeResult
  Caller->>ContextualOrchestratorJudge: submit task, answer, and criteria
  ContextualOrchestratorJudge->>ContextualOrchestrator: route evaluation prompt
  ContextualOrchestrator-->>ContextualOrchestratorJudge: return JSON response and trace usage
  ContextualOrchestratorJudge->>LLMJudgeResult: validate scores or categories
  LLMJudgeResult-->>Caller: return score, acceptance, metadata, and IRT row
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: a contextual polytomous LLM judge and an IRT response contract.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/local-llm-judge-irt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (8)
python/fast_mlsirm/llm_judge.py (4)

180-186: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove the redundant int call and clamp the lower bound.

math.floor already returns an int, which Ruff reports as RUF046. LLMJudgeResult can also be constructed directly with a score outside 0..1, because the dataclass does not validate criterion_scores. A negative score then produces a negative category index, which validate_irt_response_matrix later rejects with a confusing message.

♻️ Proposed change
         return tuple(
-            min(
-                n_categories - 1,
-                int(math.floor(float(self.criterion_scores[criterion_id]) * n_categories)),
-            )
+            min(
+                n_categories - 1,
+                max(0, math.floor(float(self.criterion_scores[criterion_id]) * n_categories)),
+            )
             for criterion_id in criterion_ids
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 180 - 186, Update the category
computation in LLMJudgeResult to remove the redundant int conversion around
math.floor and clamp the calculated category to a minimum of 0 as well as the
existing maximum of n_categories - 1, ensuring out-of-range criterion_scores
always produce valid category indices.

Source: Linters/SAST tools


425-433: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

__all__ ordering does not match Ruff RUF022 in two modules. Ruff's isort-style order lists SCREAMING_SNAKE_CASE constants before class-like names, and both new modules mix the two groups.

  • python/fast_mlsirm/llm_judge.py#L425-L433: move MAX_JUDGE_CATEGORIES, MAX_JUDGE_CRITERIA, and MAX_JUDGE_TEXT_CHARACTERS above ContextualOrchestratorJudge.
  • python/fast_mlsirm/irt_contract.py#L80-L80: move MIN_IRT_ITEMS before IRTItemType.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 425 - 433, Reorder the __all__
entries in python/fast_mlsirm/llm_judge.py lines 425-433 so
MAX_JUDGE_CATEGORIES, MAX_JUDGE_CRITERIA, and MAX_JUDGE_TEXT_CHARACTERS precede
ContextualOrchestratorJudge while preserving the remaining order. Also reorder
__all__ in python/fast_mlsirm/irt_contract.py line 80 so MIN_IRT_ITEMS precedes
IRTItemType, with no other changes.

Source: Linters/SAST tools


338-359: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Optional: harden the prompt delimiters against tag injection.

task, answer, and reference_block are inserted between literal <task>, <answer>, and <reference> tags. An answer that contains </answer> can close the block and place attacker text in the judge instruction region. The system message tells the model to treat the content as data, which reduces but does not remove the risk. Use per-call random delimiters, or send the payload as a single JSON object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 338 - 359, Harden the prompt
construction in the message-building flow by replacing predictable XML-like
delimiters around task, answer, and reference_block with per-call random
delimiters or a single JSON payload. Ensure the judge receives all three values
as data without allowing embedded closing tags to alter instruction boundaries,
while preserving the existing criterion_payload and system guidance.

365-367: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse of the accepted name hides the validation result.

Line 365 binds the parsed advisory value to accepted. Line 409 overwrites it with the runtime-derived value. The validation at line 366 is still effective, but the shared name suggests the parsed value is used. Rename the parsed value.

♻️ Proposed change
-        accepted = parsed.get("accepted")
-        if accepted is not None and not isinstance(accepted, bool):
+        advisory_accepted = parsed.get("accepted")
+        if advisory_accepted is not None and not isinstance(advisory_accepted, bool):
             raise JudgeFormatError("accepted must be a boolean when present")

Also applies to: 409-409

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 365 - 367, Rename the parsed
advisory value currently assigned from parsed.get("accepted") in the
judge-format validation flow to a distinct name, and update the associated
None/type check to use it. Keep the runtime-derived accepted value at line 409
under its existing name without changing validation behavior.
tests/test_llm_judge.py (2)

121-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the orchestrator failure paths.

The suite covers malformed JSON and non-integral categories. It does not cover a completion that omits answer, a completion that omits rationale, or a completion that is not a mapping. These paths currently raise different exception types, as flagged in python/fast_mlsirm/llm_judge.py at lines 363-368. Tests here would lock the error contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 121 - 141, Extend the LLM judge tests
around ContextualOrchestratorJudge.judge to cover payloads missing answer,
payloads missing rationale, and non-mapping completions. Assert the specific
exception type and expected error behavior for each path, preserving the
existing malformed-JSON and non-integral-category coverage.

66-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: use pytest.raises for the failure paths.

The manual try/except/else blocks duplicate what pytest.raises provides. tests/test_irt_contract.py already uses pytest.raises. Aligning both files keeps one style and shortens the tests.

♻️ Proposed change for one block
-    try:
-        ContextualOrchestratorJudge(_FakeOrchestrator("not json")).judge(
-            task="task",
-            answer="answer",
-            criteria=CRITERIA,
-        )
-    except JudgeFormatError:
-        pass
-    else:  # pragma: no cover
-        raise AssertionError("invalid judge response should fail closed")
+    with pytest.raises(JudgeFormatError):
+        ContextualOrchestratorJudge(_FakeOrchestrator("not json")).judge(
+            task="task",
+            answer="answer",
+            criteria=CRITERIA,
+        )

Add import pytest at the top of the file.

Also applies to: 113-118, 151-161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 66 - 76, Refactor the failure-path
assertions in test_judge_rejects_malformed_decisions_and_derives_acceptance and
the additional blocks around the referenced cases to use pytest.raises instead
of manual try/except/else handling. Add the pytest import and preserve each
test’s expected JudgeFormatError assertion and fail-closed behavior.
tests/test_irt_contract.py (1)

41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: escape the match pattern and add category-bound coverage.

match="0..2" is a regex. The dots match any character, so the test can pass on an unrelated message. Ruff reports RUF043 for the same reason. The suite also does not cover rejection of an out-of-range n_categories, or n_categories passed with item_type="dichotomous".

♻️ Proposed change
-    with pytest.raises(ValueError, match="0..2"):
+    with pytest.raises(ValueError, match=re.escape("0..2")):
         validate_irt_response_matrix([[0, 3]], "polytomous", n_categories=3)
+    with pytest.raises(ValueError, match="n_categories is only valid"):
+        validate_irt_response_matrix([[0, 1]], "dichotomous", n_categories=2)
+    with pytest.raises(ValueError, match="n_categories"):
+        validate_irt_response_matrix([[0, 1]], "polytomous", n_categories=1)

Add import re at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_irt_contract.py` around lines 41 - 47, Update
test_polytomous_contract_rejects_invalid_categories_and_shape to use an escaped
or otherwise literal regex for the “0..2” error match, then add coverage for
invalid n_categories bounds and for supplying n_categories with
item_type="dichotomous", asserting the appropriate ValueError messages.

Source: Linters/SAST tools

README.md (1)

92-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the ADR links and pin them to an immutable ref.

Both main URLs return 404. Correct the paths or restore the documents, then link to a commit or version tag instead of main.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 92 - 99, Update the ADR 0005 and ADR 0006 links in
the README to use valid document paths, and replace the mutable main branch
reference with an immutable commit or version tag. Preserve the existing ADR
references and descriptive text.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/fast_mlsirm/llm_judge.py`:
- Around line 72-78: Update JudgeCriterion.__post_init__ to validate
criterion_id, description, and weight types before calling regex, string, or
numeric operations, raising ValueError for invalid non-string or non-numeric
inputs. Apply the same conversion protection in _criteria around mapping weight
values so invalid inputs consistently raise ValueError rather than TypeError.
- Around line 363-368: Update the response-processing flow around _bounded_text
calls for completion["answer"] and parsed["rationale"] so malformed
model-controlled values raise JudgeFormatError instead of plain ValueError.
Preserve the existing validation and bounded-text behavior while translating
these failures into the exception type callers already handle.

---

Nitpick comments:
In `@python/fast_mlsirm/llm_judge.py`:
- Around line 180-186: Update the category computation in LLMJudgeResult to
remove the redundant int conversion around math.floor and clamp the calculated
category to a minimum of 0 as well as the existing maximum of n_categories - 1,
ensuring out-of-range criterion_scores always produce valid category indices.
- Around line 425-433: Reorder the __all__ entries in
python/fast_mlsirm/llm_judge.py lines 425-433 so MAX_JUDGE_CATEGORIES,
MAX_JUDGE_CRITERIA, and MAX_JUDGE_TEXT_CHARACTERS precede
ContextualOrchestratorJudge while preserving the remaining order. Also reorder
__all__ in python/fast_mlsirm/irt_contract.py line 80 so MIN_IRT_ITEMS precedes
IRTItemType, with no other changes.
- Around line 338-359: Harden the prompt construction in the message-building
flow by replacing predictable XML-like delimiters around task, answer, and
reference_block with per-call random delimiters or a single JSON payload. Ensure
the judge receives all three values as data without allowing embedded closing
tags to alter instruction boundaries, while preserving the existing
criterion_payload and system guidance.
- Around line 365-367: Rename the parsed advisory value currently assigned from
parsed.get("accepted") in the judge-format validation flow to a distinct name,
and update the associated None/type check to use it. Keep the runtime-derived
accepted value at line 409 under its existing name without changing validation
behavior.

In `@README.md`:
- Around line 92-99: Update the ADR 0005 and ADR 0006 links in the README to use
valid document paths, and replace the mutable main branch reference with an
immutable commit or version tag. Preserve the existing ADR references and
descriptive text.

In `@tests/test_irt_contract.py`:
- Around line 41-47: Update
test_polytomous_contract_rejects_invalid_categories_and_shape to use an escaped
or otherwise literal regex for the “0..2” error match, then add coverage for
invalid n_categories bounds and for supplying n_categories with
item_type="dichotomous", asserting the appropriate ValueError messages.

In `@tests/test_llm_judge.py`:
- Around line 121-141: Extend the LLM judge tests around
ContextualOrchestratorJudge.judge to cover payloads missing answer, payloads
missing rationale, and non-mapping completions. Assert the specific exception
type and expected error behavior for each path, preserving the existing
malformed-JSON and non-integral-category coverage.
- Around line 66-76: Refactor the failure-path assertions in
test_judge_rejects_malformed_decisions_and_derives_acceptance and the additional
blocks around the referenced cases to use pytest.raises instead of manual
try/except/else handling. Add the pytest import and preserve each test’s
expected JudgeFormatError assertion and fail-closed behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bbd1d86-ba2c-4d1c-9bcd-e3de89572a2a

📥 Commits

Reviewing files that changed from the base of the PR and between 3afb302 and f1ccb27.

📒 Files selected for processing (6)
  • README.md
  • python/fast_mlsirm/__init__.py
  • python/fast_mlsirm/irt_contract.py
  • python/fast_mlsirm/llm_judge.py
  • tests/test_irt_contract.py
  • tests/test_llm_judge.py

Comment thread python/fast_mlsirm/llm_judge.py
Comment thread python/fast_mlsirm/llm_judge.py Outdated

Copy link
Copy Markdown
Contributor Author

@opencode-agent address

Bounded exact-current repair handoff for PR #733. Before writing, refetch source head f1ccb2706a20455c959eba6fb36462257e5fec68, independently resolve protected main (currently bb30b196d2f83df5117a6bebf5e9680faf18c841 after #728), and abort if either moved or another source writer appears. Reconcile protected main non-destructively first; preserve accepted-main ATA changes and the PR's bounded six-file product slice. No force push, destructive rebase, new workflow, or parallel canonical docs branch.

Two current unresolved CodeRabbit findings are valid and must be fixed test-first on the final integrated branch: (1) public JudgeCriterion / _criteria must normalize arbitrary non-string/non-numeric criterion fields to stable ValueError rather than leaking TypeError/AttributeError; (2) malformed model-controlled completion answer / parsed rationale must surface as JudgeFormatError, not plain ValueError. Add focused regressions for missing/non-string answer, missing/non-string rationale, non-mapping completion, hostile criterion field types, and mapping weights without conversion-hook execution.

Also verify the still-current review nits before finalizing: use JSON or otherwise injection-safe data framing instead of predictable XML-like task/answer/reference delimiters; fix the broken mutable contextual-orchestrator ADR links by pinning valid immutable refs/paths; make category projection fail closed or validate criterion scores rather than silently manufacturing categories from out-of-range values; fix literal-regex/category-bound tests and RUF022 ordering where applicable. Do not merely clamp scientifically invalid scores into a valid category without an explicit validated contract.

Keep provider calls outside the psychometric core and preserve contextual-orchestrator as a read-only external dependency here. Do not persist raw task/answer/reference content beyond the call boundary or broaden provider/reviewer credentials. After fixes, require focused tests, ruff, full Python 3.12/3.14, package/Rust/PyO3/GPU-existing/fuzz, Security Scan, SAST, current-head CodeRabbit/review, zero unresolved valid threads, and live-base mergeability on one unchanged final head. Keep Ready only if all gates are genuinely satisfied.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
python/fast_mlsirm/llm_judge.py (3)

241-256: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound criteria iteration during normalization.

The maximum is checked only after the loop ends. An infinite iterable of valid criteria never reaches Line 255. It can hang the judge before any orchestrator call.

Reject the input when normalized reaches MAX_JUDGE_CRITERIA inside the loop.

Proposed fix
     normalized: list[JudgeCriterion] = []
     for value in values:
+        if len(normalized) >= MAX_JUDGE_CRITERIA:
+            raise ValueError(f"criteria must contain 1..{MAX_JUDGE_CRITERIA} values")
         if isinstance(value, JudgeCriterion):
             criterion = value

As per coding guidelines: "For fuzzed arbitrary input, code must either succeed or raise a documented benign exception; panics, hangs, ... are bugs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 241 - 256, Update _criteria to
enforce the MAX_JUDGE_CRITERIA limit during iteration, raising the existing
documented ValueError as soon as normalized reaches the maximum rather than
waiting for the iterable to end. Preserve the current normalization of valid
JudgeCriterion and mapping values, and retain the final check for empty input.

Source: Coding guidelines


128-128: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate criterion-score keys before sorting.

LLMJudgeResult is a public API. Mixed criterion_scores keys cause sorted() to raise TypeError before JudgeFormatError. Validate that criterion_scores is a mapping with string keys before sorting. Validate that criterion_categories, when provided, is a mapping before converting its keys to a set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` at line 128, Update the LLMJudgeResult
validation flow before sorting criterion_scores: verify criterion_scores is a
mapping whose keys are all strings, raising JudgeFormatError for invalid input
instead of allowing sorted() to raise TypeError. When criterion_categories is
provided, validate that it is a mapping before converting its keys to a set;
preserve normal processing for valid inputs.

Source: Coding guidelines


113-210: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a primary psychometric citation to ADR 0006.

ADR 0006 already keeps equal-width projection experimental and blocks production IRT claims. It does not provide a full primary-source basis for mapping continuous scores to equal-width MLSIRM/MLS2PLM categories. Keep LLMJudgeResult.to_irt_row non-production until validated cut-points and calibration evidence exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/fast_mlsirm/llm_judge.py` around lines 113 - 210, Add a primary
psychometric citation to ADR 0006 supporting the mapping of continuous scores
into equal-width MLSIRM/MLS2PLM categories, while preserving the existing
experimental and non-production status of LLMJudgeResult.to_irt_row until
validated cut-points and calibration evidence are available.

Source: Coding guidelines

🧹 Nitpick comments (3)
tests/test_llm_judge.py (3)

164-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test malformed answer and rationale types.

The current cases cover missing fields and a non-mapping completion. Add completions with a non-string answer and a non-string rationale. Each case must raise JudgeFormatError instead of leaking ValueError or TypeError.

The PR objective requires JudgeFormatError for malformed completion answers and rationales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 164 - 177, Extend
test_judge_rejects_missing_or_malformed_model_fields with completion mappings
whose answer and rationale fields contain non-string values, and assert each
raises JudgeFormatError. Ensure the cases continue covering the existing
missing-field and non-mapping inputs.

155-161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add out-of-range category regression cases.

This test covers only a fractional value. Also test -1 and category_count for category_count=3. Both values must raise JudgeFormatError; otherwise an accidental clamp or acceptance bug can create invalid IRT rows.

The PR objective requires category projection to fail closed or validate criterion scores instead of silently accepting out-of-range values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 155 - 161, Add regression cases
alongside the existing fractional-value test in the contextual judge tests,
using category scores of -1 and category_count (3) with category_count=3. Assert
that each raises JudgeFormatError, preserving the existing integer-format
validation and ensuring out-of-range scores are rejected rather than clamped or
accepted.

73-76: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover all untrusted prompt fields and JSON edge cases.

The test checks only task and answer. Pass a reference answer and values containing quotes, newlines, and instruction-like text. Assert that task, answer, reference, and criteria round-trip through json.loads as data.

The PR objective requires injection-safe JSON or equivalent framing for task, answer, and reference data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 73 - 76, Expand the test around the
orchestrator prompt payload to supply reference-answer and criteria values
containing quotes, newlines, and instruction-like text, alongside task and
answer. After parsing with json.loads, assert that task, answer, reference, and
criteria exactly round-trip as data, covering all untrusted prompt fields and
confirming injection-safe framing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/fast_mlsirm/llm_judge.py`:
- Around line 80-85: Update the weight validation in the criterion
initialization flow to accept only exact built-in int and float types before
calling float(), rejecting numeric subclasses and preventing custom __float__
execution. Preserve the existing TypeError for non-numeric values and the
finite-number ValueError for conversion failures.

In `@tests/test_llm_judge.py`:
- Around line 179-185: Update test_judge_criteria_reject_invalid_runtime_types
to expect ValueError for invalid JudgeCriterion fields, and add mapping-based
_criteria cases covering invalid criterion identifiers, descriptions, and
weights. Include a weight object that detects or rejects unintended
conversion-hook invocation, preserving the stable ValueError contract for both
public JudgeCriterion and _criteria validation.

---

Outside diff comments:
In `@python/fast_mlsirm/llm_judge.py`:
- Around line 241-256: Update _criteria to enforce the MAX_JUDGE_CRITERIA limit
during iteration, raising the existing documented ValueError as soon as
normalized reaches the maximum rather than waiting for the iterable to end.
Preserve the current normalization of valid JudgeCriterion and mapping values,
and retain the final check for empty input.
- Line 128: Update the LLMJudgeResult validation flow before sorting
criterion_scores: verify criterion_scores is a mapping whose keys are all
strings, raising JudgeFormatError for invalid input instead of allowing sorted()
to raise TypeError. When criterion_categories is provided, validate that it is a
mapping before converting its keys to a set; preserve normal processing for
valid inputs.
- Around line 113-210: Add a primary psychometric citation to ADR 0006
supporting the mapping of continuous scores into equal-width MLSIRM/MLS2PLM
categories, while preserving the existing experimental and non-production status
of LLMJudgeResult.to_irt_row until validated cut-points and calibration evidence
are available.

---

Nitpick comments:
In `@tests/test_llm_judge.py`:
- Around line 164-177: Extend
test_judge_rejects_missing_or_malformed_model_fields with completion mappings
whose answer and rationale fields contain non-string values, and assert each
raises JudgeFormatError. Ensure the cases continue covering the existing
missing-field and non-mapping inputs.
- Around line 155-161: Add regression cases alongside the existing
fractional-value test in the contextual judge tests, using category scores of -1
and category_count (3) with category_count=3. Assert that each raises
JudgeFormatError, preserving the existing integer-format validation and ensuring
out-of-range scores are rejected rather than clamped or accepted.
- Around line 73-76: Expand the test around the orchestrator prompt payload to
supply reference-answer and criteria values containing quotes, newlines, and
instruction-like text, alongside task and answer. After parsing with json.loads,
assert that task, answer, reference, and criteria exactly round-trip as data,
covering all untrusted prompt fields and confirming injection-safe framing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98402257-7f0c-41f7-8f1e-7ac0f3340a4c

📥 Commits

Reviewing files that changed from the base of the PR and between f1ccb27 and ff7e665.

📒 Files selected for processing (5)
  • README.md
  • python/fast_mlsirm/irt_contract.py
  • python/fast_mlsirm/llm_judge.py
  • tests/test_irt_contract.py
  • tests/test_llm_judge.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • README.md
  • python/fast_mlsirm/irt_contract.py
  • tests/test_irt_contract.py

Comment thread python/fast_mlsirm/llm_judge.py Outdated
Comment thread tests/test_llm_judge.py
@seonghobae
seonghobae marked this pull request as ready for review August 11, 2026 12:55
@seonghobae
seonghobae enabled auto-merge (squash) August 11, 2026 13:15

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head f1be6656b02b89bda7f82174091c87b7d9263bcc.

  • Head SHA: f1be6656b02b89bda7f82174091c87b7d9263bcc

  • Workflow run: 31498432384

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test (2 files)"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test (2 files)"]
  R2 --> V2["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: bcfa4e9f02bbd03811cb330d91b73a0e0302943b
  • Workflow run: 31504043242
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head bcfa4e9f02bbd03811cb330d91b73a0e0302943b.

  • Head SHA: bcfa4e9f02bbd03811cb330d91b73a0e0302943b

  • Workflow run: 31504043242

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test (2 files)"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test (2 files)"]
  R2 --> V2["targeted test run"]
Loading

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head bcfa4e9f02bbd03811cb330d91b73a0e0302943b.

  • Head SHA: bcfa4e9f02bbd03811cb330d91b73a0e0302943b

  • Workflow run: 31504043242

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test (2 files)"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test (2 files)"]
  R2 --> V2["targeted test run"]
Loading

@seonghobae
seonghobae disabled auto-merge August 11, 2026 14:56
@seonghobae

Copy link
Copy Markdown
Contributor Author

Safety hold at current head bcfa4e9f02bbd03811cb330d91b73a0e0302943b: the Strix check passed via a neutral/no-structured-report path after provider 429/410 failures. Per contextual-orchestrator ADR 0004, this is insufficient security evidence while the cross-repository trusted-gate semantics are being aligned, so auto-merge is disabled until a structured report or an authorized security-owner decision exists. No code or required-check bypass is being used.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Documentation follow-up on current head c252bdf:

  • The Judge/IRT README now links the shared contextual-orchestrator ADR 0004, pinned to contextual head 0168d4c, so exact-head review, structured Strix evidence, and no-report blocking policy are discoverable from both repositories.
  • The pre-existing working-tree change .Jules/palette.md remains intentionally uncommitted and untouched.

This is a documentation-only follow-up; rerun the full required checks and current-head review. The prior neutral/no-report Strix result remains insufficient evidence until a structured report is produced.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Linked security remediation on current head 1ff2cff:

  • The latest contextual-orchestrator Strix scan found a structured CRITICAL Dependabot cooldown issue; fast-mlsirm had the same missing cooldown pattern across pip, cargo, fuzz/cargo, and GitHub Actions.
  • Added cooldown.default-days: 7 to all four fast-mlsirm Dependabot entries and added tests/test_dependabot_config.py to require one explicit cooldown per ecosystem.
  • Updated README policy links to contextual-orchestrator ADR 0004 and ADR 0009 at immutable contextual commit befa094784e37947841948fb42016de7e6b965ab.

Local validation: YAML contract, Ruff, and the targeted metadata test pass. New required checks and a fresh structured Strix report are required on 1ff2cff.

@seonghobae
seonghobae merged commit 914127b into main Aug 11, 2026
35 checks passed
@seonghobae
seonghobae deleted the codex/local-llm-judge-irt branch August 11, 2026 16:05
@seonghobae

Copy link
Copy Markdown
Contributor Author

Post-merge audit record:

PR #733 merged as 914127ba227d3e02d0564aeeb4f27d76137610f9 at 2026-08-11T16:05:14Z under branch protection with requiredApprovals=0, while the latest review decision remained CHANGES_REQUESTED and the Strix job was a provider-outage/no-report neutral pass. The linked contextual-orchestrator ADR 0004 now records this scheduler-policy drift and requires independent current-head approval plus structured Strix evidence for future linked merges.

The merged tree does contain the Dependabot cooldown policy and its regression test. This comment preserves the governance discrepancy as follow-up evidence; it is not treated as proof that the review/Strix acceptance boundary was satisfied.

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