Skip to content

feat(core,ui): score functions — per-objective computed scores - #393

Open
j-or wants to merge 35 commits into
mainfrom
add-quality-function
Open

feat(core,ui): score functions — per-objective computed scores#393
j-or wants to merge 35 commits into
mainfrom
add-quality-function

Conversation

@j-or

@j-or j-or commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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-format
migration: 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

  • core: v21 schema + migration; 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.
  • ui: settings dialog (expression field with symbol highlighting,
    factor/response insert buttons, live playground, help dialog); data-point
    editor (f(x) toggle, manual person-icon, grid-aligned response inputs, comma
    decimals); an in-house HighlightField — no react-simple-code-editor
    dependency.

Behaviour notes

  • New data points default to using a defined function; pre-existing points stay
    manual, so existing hand-entered scores are never overwritten.

Testing

  • core and ui unit suites pass; both packages build.
  • Verified end-to-end against the real optimizer/backend via dev:local.

🤖 Generated with Claude Code

j-or and others added 30 commits September 25, 2025 12:56
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>
Jack Ord and others added 5 commits July 24, 2026 08:27
- 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>
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