feat(core,ui): score functions — per-objective computed scores - #393
Open
j-or wants to merge 35 commits into
Open
feat(core,ui): score functions — per-objective computed scores#393j-or wants to merge 35 commits into
j-or wants to merge 35 commits into
Conversation
Reconcile 253 commits of main. Conflicts resolved: - packages/ui/package.json: took main's dependency versions, kept mathjs - package-lock.json: regenerated from main's lockfile + mathjs
…state Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in data table Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rows to manual, harden response/expression handling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It is a CJS-only dependency; when bundled, rolldown rewrites its internal
require("react") to a runtime __require shim that throws in an ESM browser
environment. Externalizing it (like the other third-party deps) lets the
consumer resolve it (it is a declared dependency) and its bundler handle
the CJS/ESM interop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vite's optimized-dep interop exposes the CJS module's default export as the
whole exports object ({ __esModule: true, default: Component }) rather than the
component, so <Editor> rendered as an object -> React 'Element type is invalid'.
Unwrap .default defensively; still yields the component under vitest/bundled
interop where the default import is already the component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…expression registerAndInsert fired two setDraft calls in one handler (add the variable, then insert its symbol into the expression) both built from the same stale `draft` closure, so the expression update clobbered the variable addition. Result: a clicked factor (or response) ended up referenced in the expression but missing from `variables`, so computeScore hit an undefined mathjs symbol and returned undefined -> the factor never affected the score. Make setDraft a functional updater so the two updates compose. +regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, error-on-blur, close-on-save - Data-point Save was gated on validation violations, which come from persisted state and can't clear without saving -> unbreakable deadlock once the missing-response warning fired. Gate on isModified only; the row is still excluded from the optimizer via meta.valid and the warning still shows. - Playground built a test input for every symbol in the expression, so typing bare identifiers spawned phantom fields. Only build inputs for declared variables. - Expression parse error now shows on blur, not on every keystroke. - Settings 'Save' now closes the panel (caller onSave was a no-op). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The data-point RESPONSE header rendered the internal score name ("quality",
CSS-capitalized) instead of the objective's human label ("Quality (0-5)") used
everywhere else. Carry sv.label through TableDataRow.scoreFunctions and render it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking an insert button blurred the expression field, firing the on-blur parse error; its InfoBox renders above the buttons and shifted them down between mousedown and mouseup, cancelling the click so the symbol was never inserted. preventDefault on mousedown keeps focus (and cursor) on the textarea: no blur error, no layout shift, click fires, symbol inserts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…title Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion A score function that uses only factors (no response variables) rendered its label with no inputs beside it (e.g. a factor-only 'Cost'). Only show an objective when it has response vars, and hide the whole section when none do. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…function' button Playground is hidden by default behind a Science-icon 'Test your function' toggle; clicking reveals it. It resets to collapsed whenever the settings panel is closed (that unmounts the component). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'Test your function' toggle already labels the section, so the inner 'Playground' header and 'Test your score function here' info box were redundant. Expanding now shows the playground inputs directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The help question-mark icon (Help tooltip) now opens a dialog explaining how score functions work: factors vs responses, building the expression, the playground, and the per-row f(x)/manual toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recomputeScore now creates the {type:'score'} entry when a data point has
none yet (a new function-mode point's blank score column is dropped on
conversion), so its computed value is persisted and the point validates. The
created entry is inserted in canonical (defaultSorted) order so multi-objective
score columns stay aligned across points. Reviewed for the yi/calculateXi/
request-hash ordering invariant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Row score-function state (useDataPoints/buildScoreFunctions): a data point with no stored responses defaults to manual (keeps its entered value), while the 'add new point' template defaults to function mode — so new points use a defined function by default while pre-existing points are left untouched. Also carries the objective's human label through to the table row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Help icon opens a 'How score functions work' dialog. - 'Score function' + 'Test your function' section titles; playground always visible (no collapse), Result shown in a labelled read-only field. - Typing a factor's name auto-registers it (highlights + shows in playground), matching the button. - Buttons/inputs show the factor's real name/casing; playground inputs labelled by variable name, not the mathjs symbol. - Expression parse error shows on blur, not per keystroke. - Insert buttons no longer steal focus (preventDefault) so a blur-error layout shift can't cancel the click. - Expression field height-capped with scroll and minWidth:0 so a long expression wraps instead of overlapping the playground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Responses laid out in a grid so each objective's Nth input aligns in a column; inputs content-sized with a maxWidth cap (not stretched). - Objectives with no response variables are hidden (and the whole section when none apply). - Response inputs are plain text and accept a comma decimal separator (normalised to a dot on save), matching factor inputs. - Row Save is gated only on isModified, not on validation violations, so a row with a missing-response warning can still be saved to fix it. - Settings Save closes the panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ight field Adds a decoupled, generic HighlightField (@ui/common): a transparent textarea stacked over a highlighted <pre> in one CSS-grid cell, taking a highlight(code) => html fn and exposing insertAtCursor. ScoreFunctionField becomes a thin wrapper supplying the blue-symbol highlighting; its public API is unchanged. Removes the react-simple-code-editor dependency entirely (package.json + lockfile), so it no longer needs externalizing from the ui build and the dev:local symlink workaround is gone — the highlighter now bundles into the dist. Also drops the two CJS-interop workarounds the dependency required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the score entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Names, drop dead code - Gate the quality 0-5 InfoBox on scoreNames[0] instead of a bare 'quality' literal (keeps the objective identity check in one place). - Remove the leftover commented-out Result Typography block in the playground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'does not recompute a row in manual mode' test set no manual value and only asserted .not.toBe(200), which passed merely because the score stayed 0. Now it seeds a real manual score of 9 and asserts it is preserved exactly (toBe(9)) after the function changes — catching any recompute, not just the value 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds per-objective score functions: define a mathjs expression over named
"responses" (values measured per data point) and existing factors that computes
an objective's quality/cost score, instead of typing it by hand — with a per-row
toggle to fall back to a manually entered value.
Approach
Purely additive to the experiment data model in
@boostv/process-optimizer-frontend-core, behind a v21 data-formatmigration:
scoreVariable.scoreFunction+dataEntry.responses. The computed(or manual) score keeps living in the existing
{ type: 'score', name, value }entry, so the optimizer request, Pareto selection, plots, and CSV export are all
unchanged. See ADR 0003.
Highlights
computeScore/deriveSymbol/usedSymbols; reducer actions (updateScoreFunction,updateDataPointResponses) with recompute + canonical score-entry ordering;validation that a function-mode row with missing responses is invalid and
excluded from the optimizer.
factor/response insert buttons, live playground, help dialog); data-point
editor (
f(x)toggle, manual person-icon, grid-aligned response inputs, commadecimals); an in-house
HighlightField— noreact-simple-code-editordependency.
Behaviour notes
manual, so existing hand-entered scores are never overwritten.
Testing
dev:local.🤖 Generated with Claude Code