Repository navigation
[SC-19086] Send each figure's position in its test result - #570
Conversation
Figure keys are "<result_id>:<4 random chars>" and change on every run. They must stay unique per run, because the platform stores the image at figures/<key>/<key>.png and a stable key would overwrite older runs' images. Uploads also run concurrently, so the platform cannot read an order from them. Each figure now sends metadata._index, its position in result.figures when the result is logged. The platform uses it to find the same figure after a re-run (live figure embeds in documents). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juanmleng
left a comment
There was a problem hiding this comment.
Review: #570 at 1d5da51 (merge-base cc64d40)
Verdict: Merge with recommendations
Computed by the calibrated-review skill, run by @juanmleng.
Summary
- What changed: Each logged figure now sends its position in the test result as
metadata._index, so the platform can find the same figure after a re-run. - Result: No blocking issues. One small fix to the PR body, one item for later, and one design question.
- Main thing to know: Position works as identity only while the figure list keeps the same shape between runs. When it changes, an embed can show a different figure instead of "Result not found".
- Next step: Edit the PR body (FN1), answer Q1 here, then merge.
Fix now
FN1
- What: The PR body names the Shortcut story twice ("PRs on SC-…").
- Why: This repo is public. The story id belongs in the title only.
- Where: PR description, "What and why?" and "Dependencies" sections.
- Suggested fix: Link backend#3882 and frontend#3066 instead.
FN2
- What: Nothing checks that
_indexis left out for a figure that was never logged (index is None). - Why: This is the backward-compatible branch of
serialize(). A one-line assertion pins it. - Where:
validmind/vm_models/figure.py:149,tests/test_results.py - Suggested fix: Assert that
"_index"isn't in the metadata ofFigure(...).serialize().
Fix later
FL1. On the platform side, the embed matcher falls back to "the result's only figure" even when the embed recorded an _index that the new run doesn't have. Say a test logged two figures, an embed points at the second, and the next run logs one. The embed then shows the first figure under the second one's caption, both in the app and in exports. This doesn't block this PR because the matcher lives in the already-merged backend change. The fallback should apply only to embeds without an _index, meaning those from older libraries. Suggested owner: the backend change's author.
Questions for a human
Q1
- Owner: @cachafla
- Question: Is the figure's position a stable enough identity, or should each figure also send something that describes it, so the platform can refuse a mismatch?
- Context: Comparison tests (
validmind/tests/comparison.py:222) concatenate figures in input-combination order under the same result id. Re-running ROC Curve with datasets[train, test]and then[test, train]keeps both positions, but an embed of the test-set curve now shows the train-set curve.add_figure/remove_figureshift positions the same way. The third acceptance criterion rules out a wrong figure for older libraries. This is the same outcome for the new one. One option is to send the sorted input ids and params alongside_index, and require them to match on the platform.
Accepted risks
- What I checked: Whether any figure upload bypasses
_pre_serialize_figures, which would leave_indexunset. - Why it is acceptable:
alog_figureis called only fromTestResult.log_async, which runs_pre_serialize_figuresfirst, andlog()runs it too.
- What I checked: Comparison tests reuse the sub-results'
Figureobjects, soindexgets overwritten. - Why it is acceptable: Sub-results aren't logged. Only the combined result is, and its index is set right before upload.
- What I checked: An older platform receiving
_index. - Why it is acceptable: It's an extra metadata key that older platforms ignore. Keys and upload order don't change.
Verification performed
uv run python -m unittest tests.test_resultsat 1d5da51 → 35 passedtest_test_result_log_async_sends_each_figures_positionwithvalidmind/vm_modelsreverted to main → failed, as expectedgit grep alog_figure→ one call site, after_pre_serialize_figures- Platform
find_figureread on backend main → matches key, then_index, then only figure - PR body claim that manual
log_figurerequests carry_index0 and 1 → NOT CHECKED
Review feedback: pin the backward-compatible branch of Figure.serialize(), where index is None and metadata has no _index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review question: a figure's position is not a safe identity for comparison tests. They append each combination's figures in input order, so running ROC Curve on [train, test] and then on [test, train] keeps both positions but swaps the curves. Each figure of a comparison result now sends metadata._variant, its sub-result's inputs and params as sorted JSON. The platform takes a figure at the same position only from the same variant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@juanmleng thank you for the review. Here is what changed for each item: FN1: the PR body has no story ids now. It links validmind/backend#3882 and validmind/frontend#3066 instead. FN2: FL1: fixed on the platform, because #3882 and #3066 were already merged: validmind/backend#3891 and validmind/frontend#3072. When the embed's figure had an Q1: position alone is not enough for comparison tests, as you showed. In 2d1ef94, each figure of a comparison result also sends CI: the failed "tests" job at 2d1ef94 is |
juanmleng
left a comment
There was a problem hiding this comment.
Thanks — all four addressed. Checked 2d1ef94: tests pass, the comparison test fails without the change, and validmind/backend#3891's matcher matches what you described. Approving; worth merging validmind/backend#3891 and validmind/frontend#3072 before or with this one.
Pull Request Description
What and why?
Figures that AI drafting embeds in a document showed "Result not found" after the test ran again. The library keys each figure
<result_id>:<4 random chars>(validmind/tests/output.py,TestResult.add_figure), and makes a new key on every run, so the key an embed holds is gone after the next run.The keys must stay unique per run. The platform stores each image at
figures/<key>/<key>.png, so a stable key such as<result_id>:<n>would overwrite the image files of older runs, and the test history would show the newest image. Uploads also run at the same time (asyncio.gather), so the platform cannot read an order from them.So each figure now sends its position in the test result:
Figurehas a newindexfield.TestResult._pre_serialize_figures(it runs before every upload, inlog()andlog_async()) setsfigure.indexto the figure's position inresult.figures.Figure.serialize()sends it asmetadata._index. Keys do not change.Comparison tests also send
metadata._variant: the input and param combination of the sub-result that made the figure (sorted JSON, set in_combine_figures). They put each combination's figures one after another in input order, so a position alone can point at another combination's figure after a re-run with the inputs in another order. The platform matches a position only within the same variant (validmind/backend#3891, validmind/frontend#3072).The platform uses
_indexto find the same figure after a re-run (validmind/backend#3882 and validmind/frontend#3066, both merged).How to test
uv run python -m unittest tests.test_results tests.test_comparison_figures: 37 pass.test_each_figure_sends_the_combination_that_made_itfails without the change (no_variant) and passes with it.test_figure_serialize_omits_index_until_loggedchecks that a figure that was never logged sends no_index.test_test_result_log_async_sends_each_figures_positionfails without the change (no_indexin the metadata) and passes with it.validmind.model_validation.sklearn.ROCCurveon two datasets, and log it. Eachlog_figurerequest hasmetadatawith_index0 and 1.What needs special review?
TestResultClasses(for exampletest_test_result_log_async) are on a plainunittest.TestCase, so they are never awaited. They pass without running, and they also callMockAsyncResponsewith ajson=argument that it does not accept. The new test is synchronous and usesasyncio.run, so it really runs. I did not change the existing ones.Dependencies, breaking changes, and deployment notes
_index, the platform falls back to the exact key, then to the result's only figure.Release notes
Figures now record their position in a test result, so documents that embed a figure keep showing it after the test is run again.
Checklist
🤖 Generated with Claude Code