fix: isolate per-pair failures in evaluateItems instead of discarding the batch - #150
fix: isolate per-pair failures in evaluateItems instead of discarding the batch#150adnanrhussain wants to merge 1 commit into
Conversation
c2bb5ab to
f5ed207
Compare
f5ed207 to
2290bfd
Compare
There was a problem hiding this comment.
Pull request overview
Improves the TypeScript SDK’s Math Standards Alignment evaluator to preserve partial results in batch modes (evaluateItems, evaluateByGrade) by isolating per-pair and per-item failures, while keeping evaluate() as a throwing primitive. This prevents one bad (question, standard) pair (or one invalid item) from discarding the rest of the batch’s completed work.
Changes:
- Update
evaluateItemsto return per-pairerrorresults (with correctstatementCode) instead of rejecting the batch on the first failure. - Make question validation per-item and adjust progress reporting denominator to exclude invalid items / filtered pairs.
- Extend
evaluateByGradeaggregation to trackevaluatedCount,errorCount, andfilteredCount, and share an instance-wide LLM concurrency limiter across concurrent calls.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| sdks/typescript/src/evaluators/math/standards-alignment.ts | Implements per-pair error isolation, per-item validation behavior, by-standard metrics, and shared instance concurrency limiting. |
| sdks/typescript/tests/unit/evaluators/math/standards-alignment.test.ts | Adds unit tests covering isolated failures, correct attribution, progress reconciliation, by-standard accounting, and shared concurrency behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2290bfd to
895a022
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
895a022 to
99799a6
Compare
|
Fixed the P0 and finding 4 in P0 (progress inside try/catch) — confirmed and fixed. Finding 4 (error channel dropped detail) — Finding 3 — fixed, and it was the same defect as my own error fix: a zero-learning-component pair was landing in Finding 2 (item with empty statementCodes loses its validation error) — real, deferred. Surfacing it needs an item-level error channel on the return type, which is a wider API change than this PR should carry, and the input is doubly degenerate (invalid question and no codes). Worth its own issue. Finding 5 (options.concurrency builds a fresh limiter) — working as documented, not a defect. It is the deliberate escape hatch; the doc comment states that N concurrent calls each supplying it get N times the budget, and the demo server needs exactly that to avoid queueing interactive requests behind a large batch. Finding 6 (demo route) — correct, and a genuine follow-up: the demo pins the published Mutation tested: all four changed regions fully killed; the two new tests kill 9 mutants (89 → 95 survivors without them). File is at 69.8% on covered code, up from 64.2%. |
… the batch evaluateItems collected per-pair outcomes with allSettled and then rethrew the first rejection, so one typo'd statement code, one 429, or one malformed model response discarded every other pair's completed LLM work. Failing pairs now return a StandardAlignmentResult carrying `error` and the correct statementCode. Input validation is per item rather than a precondition for the whole call. evaluate() still throws. evaluateByGrade excludes errored pairs from coverage, and byStandard gains evaluatedCount/errorCount/filteredCount so a zero coverageCount can be told apart from an unmeasured standard. Also moves the LLM concurrency limiter to the instance so concurrent evaluateItems calls share one budget. Callers wanting per-call budgets — the demo server serves HTTP requests from one module-scope evaluator — can pass options.concurrency. Behaviour change, deliberately not marked breaking: these methods have no external consumers, and release-please has no bump-minor-pre-major setting, so a `!` would cut 1.0.0. Test `standards.some(s => s.error)` for the old semantics.
99799a6 to
369b51f
Compare
|
Three of four addressed in Empty statementCodes (your finding 4) — you were right to push back, and I was wrong to defer it. The old code threw; the new code dropped the failure entirely, which is a hole in the contract this PR introduces, not an edge case. Validation error bypassing describeFailure — correct and my inconsistency: I wrote a doc saying Zero-LC pair in no counter — added
Concurrency escape hatch — declining, final answer. It is doing what it documents: Mutation: all four changed regions fully killed; file at 71.2% covered, up from 69.8%. |
Third of four foundational SDK PRs. Independent of #148/#149 — based on
main.The bug:
evaluateItemscollected per-pair outcomes withPromise.allSettledand then didthrow errors[0], at both the pair and item level. So one typo'd code, one 429, or one malformed model response discarded every other pair's completed LLM work — one bad row in a 50-row batch threw away the other 49. The unreachable{statementCode: '', ...}fallback it left behind shows per-pair isolation was the original intent.evaluateByGradeinherits all of it, and is the mode most likely to hit one dud standard out of ~30.What changed:
errorand the correctstatementCode(available by index — the dead fallback used''because it had lost the mapping).evaluate()still throws — one pair wants an exception, 200 want a report.byStandardgainsevaluatedCount/errorCount/filteredCount/noComponentsCount, which sum to the question count so no outcome is unaccounted for.coverageCount: 0previously conflated "every pair errored", "every pair was filtered", and "genuinely didn't align" — presenting an unmeasured standard as an uncovered one.pLimit(10).Not marked breaking, deliberately. It is one in the strict sense, but these methods have no external consumers — the only in-repo caller is the demo server, pinned to the published
^0.8.0. Andrelease-please-config.jsonsets nobump-minor-pre-major, so a!would cut 1.0.0 and declare the SDK stable as a side effect of an error-handling fix. Add that setting first if a future change needs the marker. Callers wanting the old semantics:standards.some(s => s.error).Two known downstream consequences, both for the demo server (pinned to the published
^0.8.0, so neither bites until we bump it):options.concurrencyto opt out./api/evaluateroute relies onevaluateItemsrejecting to return a 500. Failures now resolve with per-standarderrorfields, so the handler must inspect them instead. Follow-up, not part of this PR.A caller's
onProgresscannot affect evaluation. Progress is counted once outside the try, and the callback runs through a guard that logs and continues — otherwise a throwing callback would discard a finished result, double-count, and reject the whole call, which is the failure this PR removes.errorcarriesname,statusCodeandretryableas well asmessageandcode, so a report can group failures by kind and separate transient ones.nameis needed because subclasses share a code.npm run lint(0 errors),typecheck,test:unit— 332 passing.Mutation tested with Stryker: 70.6% on covered code (216 killed, 90 survivors), with every changed region fully killed. The new tests were verified load-bearing rather than assumed — skipping them raises the survivor count.