From 9d1a6c4613d252b9284352e8c3966e1cb295b7cd Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 5 Dec 2024 09:18:12 -0800 Subject: [PATCH 1/6] Move scoring out of general Util into scoring util file --- dev/flipbook.tsx | 2 +- packages/perseus/src/__tests__/util.test.ts | 134 +-------------- .../src/multi-items/multi-renderer.tsx | 8 +- packages/perseus/src/renderer-util.ts | 8 +- packages/perseus/src/renderer.tsx | 3 +- packages/perseus/src/server-item-renderer.tsx | 3 +- packages/perseus/src/util.ts | 160 ------------------ packages/perseus/src/util/scoring.test.ts | 133 +++++++++++++++ packages/perseus/src/util/scoring.ts | 157 +++++++++++++++++ .../perseus/src/widgets/group/score-group.ts | 3 +- 10 files changed, 306 insertions(+), 305 deletions(-) create mode 100644 packages/perseus/src/util/scoring.test.ts create mode 100644 packages/perseus/src/util/scoring.ts diff --git a/dev/flipbook.tsx b/dev/flipbook.tsx index ea05772598..24a51280df 100644 --- a/dev/flipbook.tsx +++ b/dev/flipbook.tsx @@ -20,7 +20,7 @@ import {Renderer} from "../packages/perseus/src"; import {SvgImage} from "../packages/perseus/src/components"; import {scorePerseusItem} from "../packages/perseus/src/renderer-util"; import {mockStrings} from "../packages/perseus/src/strings"; -import {isCorrect} from "../packages/perseus/src/util"; +import {isCorrect} from "../packages/perseus/src/util/scoring"; import {trueForAllMafsSupportedGraphTypes} from "../packages/perseus/src/widgets/interactive-graphs/mafs-supported-graph-types"; import {EditableControlledInput} from "./editable-controlled-input"; diff --git a/packages/perseus/src/__tests__/util.test.ts b/packages/perseus/src/__tests__/util.test.ts index 1a0d7742cc..e3d31b49f0 100644 --- a/packages/perseus/src/__tests__/util.test.ts +++ b/packages/perseus/src/__tests__/util.test.ts @@ -1,21 +1,4 @@ -import Util, {isCorrect} from "../util"; - -describe("isCorrect", () => { - it("is true given a score with all points earned", () => { - const score = {type: "points", earned: 3, total: 3} as const; - expect(isCorrect(score)).toBe(true); - }); - - it("is false given a score with some points unearned", () => { - const score = {type: "points", earned: 2, total: 3} as const; - expect(isCorrect(score)).toBe(false); - }); - - it("is false given an unanswered / invalid score", () => { - const score = {type: "invalid"} as const; - expect(isCorrect(score)).toBe(false); - }); -}); +import Util from "../util"; describe("#constrainedTickStepsFromTickSteps", () => { it("should not changes the tick steps if there are fewer than (or exactly) 10 steps", () => { @@ -79,118 +62,3 @@ describe("deepClone", () => { expect(result[0]).not.toBe(input[0]); }); }); - -describe("flattenScores", () => { - it("defaults to an empty score", () => { - const result = Util.flattenScores({}); - - expect(result).toHaveBeenAnsweredCorrectly({shouldHavePoints: false}); - expect(result).toEqual({ - type: "points", - total: 0, - earned: 0, - message: null, - }); - }); - - it("defaults to single score if there is only one", () => { - const result = Util.flattenScores({ - "radio 1": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - }); - - expect(result).toHaveBeenAnsweredCorrectly(); - expect(result).toEqual({ - type: "points", - total: 1, - earned: 1, - message: null, - }); - }); - - it("returns an invalid score if any are invalid", () => { - const result = Util.flattenScores({ - "radio 1": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - "radio 2": { - type: "invalid", - message: null, - }, - }); - - expect(result).toHaveInvalidInput(); - expect(result).toEqual({ - type: "invalid", - message: null, - }); - }); - - it("tallies scores if multiple widgets have points", () => { - const result = Util.flattenScores({ - "radio 1": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - "radio 2": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - "radio 3": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - }); - - expect(result).toHaveBeenAnsweredCorrectly(); - expect(result).toEqual({ - type: "points", - total: 3, - earned: 3, - message: null, - }); - }); - - it("doesn't count incorrect widgets", () => { - const result = Util.flattenScores({ - "radio 1": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - "radio 2": { - type: "points", - total: 1, - earned: 1, - message: null, - }, - "radio 3": { - type: "points", - total: 1, - earned: 0, - message: null, - }, - }); - - expect(result).toEqual({ - type: "points", - total: 3, - earned: 2, - message: null, - }); - }); -}); diff --git a/packages/perseus/src/multi-items/multi-renderer.tsx b/packages/perseus/src/multi-items/multi-renderer.tsx index 29f38c9b6a..9b01836fb0 100644 --- a/packages/perseus/src/multi-items/multi-renderer.tsx +++ b/packages/perseus/src/multi-items/multi-renderer.tsx @@ -47,7 +47,7 @@ import {DependenciesContext} from "../dependencies"; import HintsRenderer from "../hints-renderer"; import {Log} from "../logging/log"; import Renderer from "../renderer"; -import Util from "../util"; +import {combineScores, keScoreFromPerseusScore} from "../util/scoring"; import {itemToTree} from "./items"; import {buildMapper} from "./trees"; @@ -404,7 +404,7 @@ class MultiRenderer extends React.Component { if (ref.getSerializedState) { state = ref.getSerializedState(); } - return Util.keScoreFromPerseusScore(score, guess, state); + return keScoreFromPerseusScore(score, guess, state); } /** @@ -441,9 +441,9 @@ class MultiRenderer extends React.Component { return data.ref?.getUserInput(); }); - const combinedScore = scores.reduce(Util.combineScores); + const combinedScore = scores.reduce(combineScores); - return Util.keScoreFromPerseusScore(combinedScore, guess, state); + return keScoreFromPerseusScore(combinedScore, guess, state); } /** diff --git a/packages/perseus/src/renderer-util.ts b/packages/perseus/src/renderer-util.ts index 7e76406a55..57d6e123a1 100644 --- a/packages/perseus/src/renderer-util.ts +++ b/packages/perseus/src/renderer-util.ts @@ -1,5 +1,5 @@ import {mapObject} from "./interactive2/objective_"; -import Util from "./util"; +import {scoreIsEmpty, flattenScores} from "./util/scoring"; import {getWidgetIdsFromContent} from "./widget-type-utils"; import {getWidgetScorer, upgradeWidgetInfoToLatestVersion} from "./widgets"; @@ -59,8 +59,8 @@ export function emptyWidgetsFunctional( locale, ); - if (score) { - return Util.scoreIsEmpty(score); + if (score != null) { + return scoreIsEmpty(score); } }); } @@ -86,7 +86,7 @@ export function scorePerseusItem( strings, locale, ); - return Util.flattenScores(scores); + return flattenScores(scores); } export function scoreWidgetsFunctional( diff --git a/packages/perseus/src/renderer.tsx b/packages/perseus/src/renderer.tsx index 95720579c9..63f4a74787 100644 --- a/packages/perseus/src/renderer.tsx +++ b/packages/perseus/src/renderer.tsx @@ -32,6 +32,7 @@ import { } from "./renderer-util"; import TranslationLinter from "./translation-linter"; import Util from "./util"; +import {flattenScores} from "./util/scoring"; import preprocessTex from "./util/tex-preprocess"; import WidgetContainer from "./widget-container"; import * as Widgets from "./widgets"; @@ -1737,7 +1738,7 @@ class Renderer this.props.strings, this.context.locale, ); - const combinedScore = Util.flattenScores(scores); + const combinedScore = flattenScores(scores); return combinedScore; } diff --git a/packages/perseus/src/server-item-renderer.tsx b/packages/perseus/src/server-item-renderer.tsx index 8e5dff99b5..b98db07c3d 100644 --- a/packages/perseus/src/server-item-renderer.tsx +++ b/packages/perseus/src/server-item-renderer.tsx @@ -21,6 +21,7 @@ import {ApiOptions} from "./perseus-api"; import Renderer from "./renderer"; import {scorePerseusItem} from "./renderer-util"; import Util from "./util"; +import {keScoreFromPerseusScore} from "./util/scoring"; import type {PerseusItem, ShowSolutions} from "./perseus-types"; import type { @@ -366,7 +367,7 @@ export class ServerItemRenderer // analyzing ProblemLogs. If not, remove this layer. const maxCompatGuess = [this.questionRenderer.getUserInput(), []]; - const keScore = Util.keScoreFromPerseusScore( + const keScore = keScoreFromPerseusScore( score, maxCompatGuess, this.questionRenderer.getSerializedState(), diff --git a/packages/perseus/src/util.ts b/packages/perseus/src/util.ts index d50781e975..baab945a3f 100644 --- a/packages/perseus/src/util.ts +++ b/packages/perseus/src/util.ts @@ -1,4 +1,3 @@ -import {Errors, PerseusError} from "@khanacademy/perseus-core"; import _ from "underscore"; import KhanAnswerTypes from "./util/answer-types"; @@ -6,9 +5,6 @@ import * as GraphieUtil from "./util.graphie"; import type {Range} from "./perseus-types"; import type {PerseusStrings} from "./strings"; -import type {PerseusScore} from "./types"; -import type {UserInputArray} from "./validation.types"; -import type {KEScore} from "@khanacademy/perseus-core"; import type * as React from "react"; type WordPosition = { @@ -95,12 +91,6 @@ const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/; const rWidgetParts = new RegExp(rWidgetRule.source + "$"); const snowman = "\u2603"; -const noScore: PerseusScore = { - type: "points", - earned: 0, - total: 0, - message: null, -}; const seededRNG: (seed: number) => RNG = function (seed: number): RNG { let randomSeed = seed; @@ -194,126 +184,6 @@ const split: (str: string, r: RegExp) => ReadonlyArray = "x".split( return output; }; -/** - * Combine two score objects. - * - * Given two score objects for two different widgets, combine them so that - * if one is wrong, the total score is wrong, etc. - */ -function combineScores( - scoreA: PerseusScore, - scoreB: PerseusScore, -): PerseusScore { - let message; - - if (scoreA.type === "points" && scoreB.type === "points") { - if ( - scoreA.message && - scoreB.message && - scoreA.message !== scoreB.message - ) { - // TODO(alpert): Figure out how to combine messages usefully - message = null; - } else { - message = scoreA.message || scoreB.message; - } - - return { - type: "points", - earned: scoreA.earned + scoreB.earned, - total: scoreA.total + scoreB.total, - message: message, - }; - } - if (scoreA.type === "points" && scoreB.type === "invalid") { - return scoreB; - } - if (scoreA.type === "invalid" && scoreB.type === "points") { - return scoreA; - } - if (scoreA.type === "invalid" && scoreB.type === "invalid") { - if ( - scoreA.message && - scoreB.message && - scoreA.message !== scoreB.message - ) { - // TODO(alpert): Figure out how to combine messages usefully - message = null; - } else { - message = scoreA.message || scoreB.message; - } - - return { - type: "invalid", - message: message, - }; - } - - /** - * The above checks cover all combinations of score type, so if we get here - * then something is amiss with our inputs. - */ - throw new PerseusError( - "PerseusScore with unknown type encountered", - Errors.InvalidInput, - { - metadata: { - scoreA: JSON.stringify(scoreA), - scoreB: JSON.stringify(scoreB), - }, - }, - ); -} - -function flattenScores(widgetScoreMap: { - [widgetId: string]: PerseusScore; -}): PerseusScore { - return Object.values(widgetScoreMap).reduce(combineScores, noScore); -} - -export function isCorrect(score: PerseusScore): boolean { - return score.type === "points" && score.earned >= score.total; -} - -function keScoreFromPerseusScore( - score: PerseusScore, - // It's weird, but this is what we're passing it - guess: UserInputArray | [UserInputArray, []], - state: any, -): KEScore { - if (score.type === "points") { - return { - empty: false, - correct: isCorrect(score), - message: score.message, - guess: guess, - state: state, - }; - } - if (score.type === "invalid") { - return { - empty: true, - correct: false, - message: score.message, - suppressAlmostThere: score.suppressAlmostThere, - guess: guess, - state: state, - }; - } - throw new PerseusError( - // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'. - "Invalid score type: " + score.type, - Errors.InvalidInput, - { - metadata: { - score: JSON.stringify(score), - guess: JSON.stringify(guess), - state: JSON.stringify(state), - }, - }, - ); -} - /** * Return the first valid interpretation of 'text' as a number, in the form * {value: 2.3, exact: true}. @@ -655,31 +525,6 @@ function strongEncodeURIComponent(str: string): string { ); } -/** - * If a widget says that it is empty once it is graded. - * Trying to encapsulate references to the score format. - */ -function scoreIsEmpty(score: PerseusScore): boolean { - // HACK(benkomalo): ugh. this isn't great; the Perseus score objects - // overload the type "invalid" for what should probably be three - // distinct cases: - // - truly empty or not fully filled out - // - invalid or malformed inputs - // - "almost correct" like inputs where the widget wants to give - // feedback (e.g. a fraction needs to be reduced, or `pi` should - // be used instead of 3.14) - // - // Unfortunately the coercion happens all over the place, as these - // Perseus style score objects are created *everywhere* (basically - // in every widget), so it's hard to change now. We assume that - // anything with a "message" is not truly empty, and one of the - // latter two cases for now. - return ( - score.type === "invalid" && - (!score.message || score.message.length === 0) - ); -} - /* * The touchHandlers are used to track the current state of the touch * event, such as whether or not the user is currently pressed down (either @@ -884,13 +729,9 @@ const Util = { rTypeFromWidgetId, rWidgetParts, snowman, - noScore, seededRNG, shuffle, split, - combineScores, - flattenScores, - keScoreFromPerseusScore, firstNumericalParse, stringArrayOfSize, gridDimensionConfig, @@ -906,7 +747,6 @@ const Util = { parseQueryString, updateQueryString, strongEncodeURIComponent, - scoreIsEmpty, touchHandlers, resetTouchHandlers, extractPointerLocation, diff --git a/packages/perseus/src/util/scoring.test.ts b/packages/perseus/src/util/scoring.test.ts new file mode 100644 index 0000000000..efddbdbd6c --- /dev/null +++ b/packages/perseus/src/util/scoring.test.ts @@ -0,0 +1,133 @@ +import {flattenScores, isCorrect} from "./scoring"; + +describe("isCorrect", () => { + it("is true given a score with all points earned", () => { + const score = {type: "points", earned: 3, total: 3} as const; + expect(isCorrect(score)).toBe(true); + }); + + it("is false given a score with some points unearned", () => { + const score = {type: "points", earned: 2, total: 3} as const; + expect(isCorrect(score)).toBe(false); + }); + + it("is false given an unanswered / invalid score", () => { + const score = {type: "invalid"} as const; + expect(isCorrect(score)).toBe(false); + }); +}); + +describe("flattenScores", () => { + it("defaults to an empty score", () => { + const result = flattenScores({}); + + expect(result).toHaveBeenAnsweredCorrectly({shouldHavePoints: false}); + expect(result).toEqual({ + type: "points", + total: 0, + earned: 0, + message: null, + }); + }); + + it("defaults to single score if there is only one", () => { + const result = flattenScores({ + "radio 1": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + }); + + expect(result).toHaveBeenAnsweredCorrectly(); + expect(result).toEqual({ + type: "points", + total: 1, + earned: 1, + message: null, + }); + }); + + it("returns an invalid score if any are invalid", () => { + const result = flattenScores({ + "radio 1": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + "radio 2": { + type: "invalid", + message: null, + }, + }); + + expect(result).toHaveInvalidInput(); + expect(result).toEqual({ + type: "invalid", + message: null, + }); + }); + + it("tallies scores if multiple widgets have points", () => { + const result = flattenScores({ + "radio 1": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + "radio 2": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + "radio 3": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + }); + + expect(result).toHaveBeenAnsweredCorrectly(); + expect(result).toEqual({ + type: "points", + total: 3, + earned: 3, + message: null, + }); + }); + + it("doesn't count incorrect widgets", () => { + const result = flattenScores({ + "radio 1": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + "radio 2": { + type: "points", + total: 1, + earned: 1, + message: null, + }, + "radio 3": { + type: "points", + total: 1, + earned: 0, + message: null, + }, + }); + + expect(result).toEqual({ + type: "points", + total: 3, + earned: 2, + message: null, + }); + }); +}); diff --git a/packages/perseus/src/util/scoring.ts b/packages/perseus/src/util/scoring.ts new file mode 100644 index 0000000000..b66af9c8f8 --- /dev/null +++ b/packages/perseus/src/util/scoring.ts @@ -0,0 +1,157 @@ +import {Errors, PerseusError} from "@khanacademy/perseus-core"; + +import type {PerseusScore} from "../types"; +import type {UserInputArray} from "../validation.types"; +import type {KEScore} from "@khanacademy/perseus-core"; + +const noScore: PerseusScore = { + type: "points", + earned: 0, + total: 0, + message: null, +}; + +/** + * If a widget says that it is empty once it is graded. + * Trying to encapsulate references to the score format. + */ +export function scoreIsEmpty(score: PerseusScore): boolean { + // HACK(benkomalo): ugh. this isn't great; the Perseus score objects + // overload the type "invalid" for what should probably be three + // distinct cases: + // - truly empty or not fully filled out + // - invalid or malformed inputs + // - "almost correct" like inputs where the widget wants to give + // feedback (e.g. a fraction needs to be reduced, or `pi` should + // be used instead of 3.14) + // + // Unfortunately the coercion happens all over the place, as these + // Perseus style score objects are created *everywhere* (basically + // in every widget), so it's hard to change now. We assume that + // anything with a "message" is not truly empty, and one of the + // latter two cases for now. + return ( + score.type === "invalid" && + (!score.message || score.message.length === 0) + ); +} + +/** + * Combine two score objects. + * + * Given two score objects for two different widgets, combine them so that + * if one is wrong, the total score is wrong, etc. + */ +export function combineScores( + scoreA: PerseusScore, + scoreB: PerseusScore, +): PerseusScore { + let message; + + if (scoreA.type === "points" && scoreB.type === "points") { + if ( + scoreA.message && + scoreB.message && + scoreA.message !== scoreB.message + ) { + // TODO(alpert): Figure out how to combine messages usefully + message = null; + } else { + message = scoreA.message || scoreB.message; + } + + return { + type: "points", + earned: scoreA.earned + scoreB.earned, + total: scoreA.total + scoreB.total, + message: message, + }; + } + if (scoreA.type === "points" && scoreB.type === "invalid") { + return scoreB; + } + if (scoreA.type === "invalid" && scoreB.type === "points") { + return scoreA; + } + if (scoreA.type === "invalid" && scoreB.type === "invalid") { + if ( + scoreA.message && + scoreB.message && + scoreA.message !== scoreB.message + ) { + // TODO(alpert): Figure out how to combine messages usefully + message = null; + } else { + message = scoreA.message || scoreB.message; + } + + return { + type: "invalid", + message: message, + }; + } + + /** + * The above checks cover all combinations of score type, so if we get here + * then something is amiss with our inputs. + */ + throw new PerseusError( + "PerseusScore with unknown type encountered", + Errors.InvalidInput, + { + metadata: { + scoreA: JSON.stringify(scoreA), + scoreB: JSON.stringify(scoreB), + }, + }, + ); +} + +export function flattenScores(widgetScoreMap: { + [widgetId: string]: PerseusScore; +}): PerseusScore { + return Object.values(widgetScoreMap).reduce(combineScores, noScore); +} + +export function isCorrect(score: PerseusScore): boolean { + return score.type === "points" && score.earned >= score.total; +} + +export function keScoreFromPerseusScore( + score: PerseusScore, + // It's weird, but this is what we're passing it + guess: UserInputArray | [UserInputArray, []], + state: any, +): KEScore { + if (score.type === "points") { + return { + empty: false, + correct: isCorrect(score), + message: score.message, + guess: guess, + state: state, + }; + } + if (score.type === "invalid") { + return { + empty: true, + correct: false, + message: score.message, + suppressAlmostThere: score.suppressAlmostThere, + guess: guess, + state: state, + }; + } + throw new PerseusError( + // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'. + "Invalid score type: " + score.type, + Errors.InvalidInput, + { + metadata: { + score: JSON.stringify(score), + guess: JSON.stringify(guess), + state: JSON.stringify(state), + }, + }, + ); +} diff --git a/packages/perseus/src/widgets/group/score-group.ts b/packages/perseus/src/widgets/group/score-group.ts index 4db4058118..35c041c8d3 100644 --- a/packages/perseus/src/widgets/group/score-group.ts +++ b/packages/perseus/src/widgets/group/score-group.ts @@ -1,5 +1,6 @@ import {scoreWidgetsFunctional} from "../../renderer-util"; import Util from "../../util"; +import {flattenScores} from "../../util/scoring"; import type {PerseusStrings} from "../../strings"; import type {PerseusScore} from "../../types"; @@ -24,7 +25,7 @@ function scoreGroup( locale, ); - return Util.flattenScores(scores); + return flattenScores(scores); } export default scoreGroup; From f366700e8b44d96a1acf44c2fe9b9f37380c0867 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 5 Dec 2024 19:07:16 -0800 Subject: [PATCH 2/6] Changeset --- .changeset/clever-cars-eat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clever-cars-eat.md diff --git a/.changeset/clever-cars-eat.md b/.changeset/clever-cars-eat.md new file mode 100644 index 0000000000..407d739cd0 --- /dev/null +++ b/.changeset/clever-cars-eat.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Move scoring utility functions out of `Util` object into their own file From 63120a9900dca6e3addc5c5d61357aef2ba127bb Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 5 Dec 2024 19:09:09 -0800 Subject: [PATCH 3/6] Revert tiny change --- packages/perseus/src/renderer-util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus/src/renderer-util.ts b/packages/perseus/src/renderer-util.ts index 57d6e123a1..6c05e18cf0 100644 --- a/packages/perseus/src/renderer-util.ts +++ b/packages/perseus/src/renderer-util.ts @@ -59,7 +59,7 @@ export function emptyWidgetsFunctional( locale, ); - if (score != null) { + if (score) { return scoreIsEmpty(score); } }); From 11f94836270ffa6cebce614fcf50a2cbb5a08e08 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 08:44:23 -0800 Subject: [PATCH 4/6] Remove unused import --- packages/perseus/src/widgets/group/score-group.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/perseus/src/widgets/group/score-group.ts b/packages/perseus/src/widgets/group/score-group.ts index 35c041c8d3..06eb2406fb 100644 --- a/packages/perseus/src/widgets/group/score-group.ts +++ b/packages/perseus/src/widgets/group/score-group.ts @@ -1,5 +1,4 @@ import {scoreWidgetsFunctional} from "../../renderer-util"; -import Util from "../../util"; import {flattenScores} from "../../util/scoring"; import type {PerseusStrings} from "../../strings"; From a3a5230cd9d277e8cd30c35f8db6284f99cba986 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 08:57:23 -0800 Subject: [PATCH 5/6] Export helper keScoreFromPerseusScore as its used externally --- packages/perseus/src/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index f16c1ecaf3..3c43155d69 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -37,8 +37,9 @@ export {default as TableWidget} from "./widgets/table"; export {default as PlotterWidget} from "./widgets/plotter"; export {default as GrapherWidget} from "./widgets/grapher"; -// Some utils in grapher/utils don't need to be used outside of `perseus`, -// so only export the stuff that does need to be exposed +// Some utils in grapher/utils and scoring don't need to be used outside of +// `perseus`, so only export the stuff that does need to be exposed +import {keScoreFromPerseusScore} from "./util/scoring"; import { allTypes, DEFAULT_GRAPHER_PROPS, @@ -57,6 +58,10 @@ export const GrapherUtil = { typeToButton, }; +export const ScoringUtil = { + keScoreFromPerseusScore, +}; + /** * Misc */ From f44e07e18b10a60e0e8dc33f2dbfcf633a988904 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 08:58:41 -0800 Subject: [PATCH 6/6] Changeset --- .changeset/clever-cars-eat.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/clever-cars-eat.md b/.changeset/clever-cars-eat.md index 407d739cd0..c7e1d59fb0 100644 --- a/.changeset/clever-cars-eat.md +++ b/.changeset/clever-cars-eat.md @@ -1,5 +1,5 @@ --- -"@khanacademy/perseus": minor +"@khanacademy/perseus": major --- -Move scoring utility functions out of `Util` object into their own file +Move scoring utility functions out of `Util` object into their own file and only export externally used function (`keScoreFromPerseusScore`)