From d93e3ecdeb6bd714a35dcd9f886299fa80ba71ec Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 08:43:30 -0800 Subject: [PATCH 1/5] Remove deprecated/unused support for examples in Renderer (keeping for numeric-input though) (#1961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: While working on SSS, I came across the `examples()` function on the `Renderer` that was causing me issues (with threading client-side validation in). I checked and it is no longer used anywhere. Note that the `input-number` and `numeric-input` use their local `examples()` functions still to pass a set of examples to the `InputWithExamples` component, which we're keeping. Implemented in [D2749](https://phabricator.khanacademy.org/D2749) Issue: LEMS-2561 ## Test plan: `yarn test` `yarn typecheck` Checked in host webapp and the function is not used. Author: jeremywiebe Reviewers: jeremywiebe, handeyeco, Myranae, mark-fitzgerald Required Reviewers: Approved By: handeyeco Checks: ⌛ Publish npm snapshot (ubuntu-latest, 20.x), ⌛ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ⌛ Cypress (ubuntu-latest, 20.x), ⌛ Check builds for changes in size (ubuntu-latest, 20.x), ⌛ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⌛ gerald, ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1961 --- .changeset/orange-wombats-destroy.md | 5 ++ .../perseus/src/__tests__/renderer.test.tsx | 68 ------------------- packages/perseus/src/renderer.tsx | 29 -------- packages/perseus/src/types.ts | 1 - .../src/widgets/input-number/input-number.tsx | 4 +- .../widgets/numeric-input/numeric-input.tsx | 9 ++- 6 files changed, 13 insertions(+), 103 deletions(-) create mode 100644 .changeset/orange-wombats-destroy.md diff --git a/.changeset/orange-wombats-destroy.md b/.changeset/orange-wombats-destroy.md new file mode 100644 index 0000000000..fb2f09445b --- /dev/null +++ b/.changeset/orange-wombats-destroy.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": major +--- + +Remove deprecated/unused `examples()` function from `Renderer` diff --git a/packages/perseus/src/__tests__/renderer.test.tsx b/packages/perseus/src/__tests__/renderer.test.tsx index 04892d6eb0..fccdbddebb 100644 --- a/packages/perseus/src/__tests__/renderer.test.tsx +++ b/packages/perseus/src/__tests__/renderer.test.tsx @@ -1793,72 +1793,4 @@ describe("renderer", () => { expect(Object.keys(json.widgets)).toEqual(widgetKeys); }); }); - - describe("examples", () => { - it("should return examples if all widgets return the same examples (or null)", () => { - // Arrange - const {renderer} = renderQuestion({ - content: - "Input widget: [[\u2603 input-number 1]]\n\n" + - "Dropdown widget: [[\u2603 dropdown 1]]\n\n" + - "Image widget (won't have user input): [[\u2603 image 1]]\n\n" + - "Another input widget: [[\u2603 input-number 2]]", - widgets: { - "image 1": imageWidget, - "input-number 1": inputNumberWidget, - "input-number 2": inputNumberWidget, - "dropdown 1": dropdownWidget, - }, - images: {}, - }); - - // Act - const examples = renderer.examples(); - - // Assert - expect(examples).toMatchInlineSnapshot(` - [ - "**Your answer should be** ", - "an integer, like $6$", - "a *proper* fraction, like $1/2$ or $6/10$", - "an *improper* fraction, like $10/7$ or $14/8$", - "a mixed number, like $1\\ 3/4$", - ] - `); - }); - - it("should return nothing if widgets return the different examples", () => { - // NOTE(jeremy): I'm unsure why we don't return examples if the - // examples aren't the same, but this is current functionality so - // I'm adding this test to verify the current behaviour. - - // Arrange - const {renderer} = renderQuestion({ - content: - "Input widget: [[\u2603 input-number 1]]\n\n" + - "Dropdown widget: [[\u2603 dropdown 1]]\n\n" + - "Image widget (won't have user input): [[\u2603 image 1]]\n\n" + - "Another input widget: [[\u2603 input-number 2]]", - widgets: { - "image 1": imageWidget, - "input-number 1": inputNumberWidget, - "input-number 2": { - ...inputNumberWidget, - options: { - ...inputNumberWidget.options, - answerType: "percent", - }, - }, - "dropdown 1": dropdownWidget, - }, - images: {}, - }); - - // Act - const examples = renderer.examples(); - - // Assert - expect(examples).toBeNull(); - }); - }); }); diff --git a/packages/perseus/src/renderer.tsx b/packages/perseus/src/renderer.tsx index 95720579c9..1d99c5c7e8 100644 --- a/packages/perseus/src/renderer.tsx +++ b/packages/perseus/src/renderer.tsx @@ -1751,35 +1751,6 @@ class Renderer return [totalGuess, totalScore]; }; - examples: () => ReadonlyArray | null | undefined = () => { - const widgetIds = this.widgetIds; - const examples = widgetIds - .map((widgetId) => { - const widget = this.getWidgetInstance(widgetId); - return widget != null && widget.examples - ? widget.examples() - : null; - }) - .filter(Boolean); - - // no widgets with examples - if (!examples.length) { - return null; - } - - const allEqual = _.all(examples, function (example) { - return _.isEqual(examples[0], example); - }); - - // some widgets have different examples - // TODO(alex): handle this better - if (!allEqual) { - return null; - } - - return examples[0]; - }; - // TranslationLinter callback handletranslationLintErrors: (lintErrors: ReadonlyArray) => void = ( lintErrors: ReadonlyArray, diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts index 41e4514cdd..7ba9d2d385 100644 --- a/packages/perseus/src/types.ts +++ b/packages/perseus/src/types.ts @@ -90,7 +90,6 @@ export interface Widget { getUserInput?: () => UserInputArray | UserInput | undefined; showRationalesForCurrentlySelectedChoices?: (options?: any) => void; - examples?: () => ReadonlyArray; getPromptJSON?: () => WidgetPromptJSON; } diff --git a/packages/perseus/src/widgets/input-number/input-number.tsx b/packages/perseus/src/widgets/input-number/input-number.tsx index fe0d31a5c1..12ac2ab6fe 100644 --- a/packages/perseus/src/widgets/input-number/input-number.tsx +++ b/packages/perseus/src/widgets/input-number/input-number.tsx @@ -175,7 +175,7 @@ class InputNumber extends React.Component implements Widget { return _getPromptJSON(this.props, this.getUserInput()); } - examples: () => ReadonlyArray = () => { + examples(): ReadonlyArray { const {strings} = this.context; const type = this.props.answerType; const forms = answerTypes[type].forms.split(/\s*,\s*/); @@ -185,7 +185,7 @@ class InputNumber extends React.Component implements Widget { ); return [strings.yourAnswer].concat(examples); - }; + } render(): React.ReactNode { if (this.props.apiOptions.customKeypad) { diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx index b138a9b7d2..bd8bf29dc6 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx @@ -123,8 +123,11 @@ export class NumericInput isFocused: false, }; - // TODO(Nicole, Jeremy): This is maybe never used and should be removed - examples: () => ReadonlyArray = () => { + /** + * Generates a string that demonstrates how to input the various supported + * answer forms. + */ + examples(): ReadonlyArray { // if the set of specified forms are empty, allow all forms const forms = this.props.answerForms?.length !== 0 @@ -144,7 +147,7 @@ export class NumericInput examples = _.uniq(examples); return [this.context.strings.yourAnswer].concat(examples); - }; + } shouldShowExamples: () => boolean = () => { const noFormsAccepted = this.props.answerForms?.length === 0; From 435280ac4cf33ee98ddb1166631f87f81cafa0fc Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 09:05:24 -0800 Subject: [PATCH 2/5] Move scoring out of general Util into scoring util file (#1962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Like #1948, I was working in `utils.ts` and found it annoying how there were so many different utils in one file (graphing support, scoring, word/text manipulation). So this just pulls scoring-related behaviour out into a new file. This means these symbols are no longer on the `Utils` object, but I checked and they aren't used in Perseus nor in consuming apps. Issue: LEMS-2561 ## Test plan: `yarn test` `yarn typecheck` Author: jeremywiebe Reviewers: jeremywiebe, Myranae, handeyeco, benchristel, SonicScrewdriver Required Reviewers: Approved By: Myranae, handeyeco Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1962 --- .changeset/clever-cars-eat.md | 5 + dev/flipbook.tsx | 2 +- packages/perseus/src/__tests__/util.test.ts | 134 +-------------- packages/perseus/src/index.ts | 9 +- .../src/multi-items/multi-renderer.tsx | 8 +- packages/perseus/src/renderer-util.ts | 6 +- 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 | 4 +- 12 files changed, 317 insertions(+), 307 deletions(-) create mode 100644 .changeset/clever-cars-eat.md create mode 100644 packages/perseus/src/util/scoring.test.ts create mode 100644 packages/perseus/src/util/scoring.ts diff --git a/.changeset/clever-cars-eat.md b/.changeset/clever-cars-eat.md new file mode 100644 index 0000000000..c7e1d59fb0 --- /dev/null +++ b/.changeset/clever-cars-eat.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": major +--- + +Move scoring utility functions out of `Util` object into their own file and only export externally used function (`keScoreFromPerseusScore`) 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/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 */ 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..6c05e18cf0 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"; @@ -60,7 +60,7 @@ export function emptyWidgetsFunctional( ); if (score) { - return Util.scoreIsEmpty(score); + 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 1d99c5c7e8..80312ba144 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..06eb2406fb 100644 --- a/packages/perseus/src/widgets/group/score-group.ts +++ b/packages/perseus/src/widgets/group/score-group.ts @@ -1,5 +1,5 @@ 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 +24,7 @@ function scoreGroup( locale, ); - return Util.flattenScores(scores); + return flattenScores(scores); } export default scoreGroup; From cfe9e67ac427770f85b423dfb37c8f1310c39442 Mon Sep 17 00:00:00 2001 From: Khan Actions Bot <56267880+khan-actions-bot@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:02:49 -0500 Subject: [PATCH 3/5] Version Packages (#1964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @khanacademy/perseus@46.0.0 ### Major Changes - [#1962](https://github.com/Khan/perseus/pull/1962) [`435280ac4`](https://github.com/Khan/perseus/commit/435280ac4cf33ee98ddb1166631f87f81cafa0fc) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move scoring utility functions out of `Util` object into their own file and only export externally used function (`keScoreFromPerseusScore`) - [#1961](https://github.com/Khan/perseus/pull/1961) [`d93e3ecde`](https://github.com/Khan/perseus/commit/d93e3ecdeb6bd714a35dcd9f886299fa80ba71ec) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Remove deprecated/unused `examples()` function from `Renderer` ## @khanacademy/perseus-editor@15.1.3 ### Patch Changes - Updated dependencies \[[`435280ac4`](https://github.com/Khan/perseus/commit/435280ac4cf33ee98ddb1166631f87f81cafa0fc), [`d93e3ecde`](https://github.com/Khan/perseus/commit/d93e3ecdeb6bd714a35dcd9f886299fa80ba71ec)]: - @khanacademy/perseus@46.0.0 Author: khan-actions-bot Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ⏭️ Publish npm snapshot, ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1964 --- .changeset/clever-cars-eat.md | 5 ----- .changeset/orange-wombats-destroy.md | 5 ----- packages/perseus-editor/CHANGELOG.md | 7 +++++++ packages/perseus-editor/package.json | 4 ++-- packages/perseus/CHANGELOG.md | 8 ++++++++ packages/perseus/package.json | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) delete mode 100644 .changeset/clever-cars-eat.md delete mode 100644 .changeset/orange-wombats-destroy.md diff --git a/.changeset/clever-cars-eat.md b/.changeset/clever-cars-eat.md deleted file mode 100644 index c7e1d59fb0..0000000000 --- a/.changeset/clever-cars-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": major ---- - -Move scoring utility functions out of `Util` object into their own file and only export externally used function (`keScoreFromPerseusScore`) diff --git a/.changeset/orange-wombats-destroy.md b/.changeset/orange-wombats-destroy.md deleted file mode 100644 index fb2f09445b..0000000000 --- a/.changeset/orange-wombats-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": major ---- - -Remove deprecated/unused `examples()` function from `Renderer` diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index e8d14badb2..91941befa8 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,12 @@ # @khanacademy/perseus-editor +## 15.1.3 + +### Patch Changes + +- Updated dependencies [[`435280ac4`](https://github.com/Khan/perseus/commit/435280ac4cf33ee98ddb1166631f87f81cafa0fc), [`d93e3ecde`](https://github.com/Khan/perseus/commit/d93e3ecdeb6bd714a35dcd9f886299fa80ba71ec)]: + - @khanacademy/perseus@46.0.0 + ## 15.1.2 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 4831646708..70db62a16c 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -3,7 +3,7 @@ "description": "Perseus editors", "author": "Khan Academy", "license": "MIT", - "version": "15.1.2", + "version": "15.1.3", "publishConfig": { "access": "public" }, @@ -38,7 +38,7 @@ "@khanacademy/keypad-context": "^1.0.4", "@khanacademy/kmath": "^0.1.16", "@khanacademy/math-input": "^21.1.6", - "@khanacademy/perseus": "^45.1.0", + "@khanacademy/perseus": "^46.0.0", "@khanacademy/perseus-core": "1.5.3", "@khanacademy/pure-markdown": "^0.3.13", "mafs": "^0.19.0" diff --git a/packages/perseus/CHANGELOG.md b/packages/perseus/CHANGELOG.md index 8cd2e8669c..4b2c5b0579 100644 --- a/packages/perseus/CHANGELOG.md +++ b/packages/perseus/CHANGELOG.md @@ -1,5 +1,13 @@ # @khanacademy/perseus +## 46.0.0 + +### Major Changes + +- [#1962](https://github.com/Khan/perseus/pull/1962) [`435280ac4`](https://github.com/Khan/perseus/commit/435280ac4cf33ee98ddb1166631f87f81cafa0fc) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move scoring utility functions out of `Util` object into their own file and only export externally used function (`keScoreFromPerseusScore`) + +* [#1961](https://github.com/Khan/perseus/pull/1961) [`d93e3ecde`](https://github.com/Khan/perseus/commit/d93e3ecdeb6bd714a35dcd9f886299fa80ba71ec) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Remove deprecated/unused `examples()` function from `Renderer` + ## 45.1.0 ### Minor Changes diff --git a/packages/perseus/package.json b/packages/perseus/package.json index f3edd36dd7..e07e2d5110 100644 --- a/packages/perseus/package.json +++ b/packages/perseus/package.json @@ -3,7 +3,7 @@ "description": "Core Perseus API (includes renderers and widgets)", "author": "Khan Academy", "license": "MIT", - "version": "45.1.0", + "version": "46.0.0", "publishConfig": { "access": "public" }, From e22a931d987291258b66f2c80db3536970a4555d Mon Sep 17 00:00:00 2001 From: Mark Fitzgerald <13896410+mark-fitzgerald@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:48:37 -0800 Subject: [PATCH 4/5] [Numeric Input] - BUGFIX - Adjust color contrast for tooltip text (#1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: The text in the numeric input tooltip was slightly out of spec for accessibility. This change ensures that the text has a minimum 4.5:1 contrast ratio. ## Test plan: 1. Launch Storybook 2. Navigate to Perseus Editor => Editor => [Demo](http://localhost:6006/?path=/story/perseuseditor-editorpage--demo) 3. Add a Numeric Input widget 4. Configure the widget to have any number of format options 5. Place focus in the input field in the preview for the widget 6. Check the color value in the browser inspector for the text in the tooltip (it should be greater than 4.5:1) ## Affected UI: ### Before ![Color Contrast - Before](https://github.com/user-attachments/assets/2079d040-f240-45d1-91b7-a57fb062df5d) ### After ![Color Contrast - After](https://github.com/user-attachments/assets/3d064dd2-061e-4d8e-957f-3d362be01d7f) Author: mark-fitzgerald Reviewers: jeremywiebe, mark-fitzgerald, nishasy Required Reviewers: Approved By: jeremywiebe, nishasy Checks: ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald, ❌ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1966 --- .changeset/poor-numbers-reflect.md | 5 +++++ packages/perseus/src/styles/perseus-renderer.less | 2 +- packages/perseus/src/styles/variables.less | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-numbers-reflect.md diff --git a/.changeset/poor-numbers-reflect.md b/.changeset/poor-numbers-reflect.md new file mode 100644 index 0000000000..4830ff7a54 --- /dev/null +++ b/.changeset/poor-numbers-reflect.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +[Numeric Input] - BUGFIX - Adjust color contrast of tooltip text diff --git a/packages/perseus/src/styles/perseus-renderer.less b/packages/perseus/src/styles/perseus-renderer.less index 3e02cc2162..e19530ae4e 100644 --- a/packages/perseus/src/styles/perseus-renderer.less +++ b/packages/perseus/src/styles/perseus-renderer.less @@ -544,7 +544,7 @@ .perseus-formats-tooltip { background: #fff; - color: #777; + color: @textSecondary; padding: 5px 10px; width: 240px; } diff --git a/packages/perseus/src/styles/variables.less b/packages/perseus/src/styles/variables.less index df1ca00076..0467ef6230 100644 --- a/packages/perseus/src/styles/variables.less +++ b/packages/perseus/src/styles/variables.less @@ -21,6 +21,8 @@ @gray25: #3b3e40; @gray17: #21242c; +@textSecondary: #717378; + // Media query breakpoints // -------------------------------------------------- From e49f6f7d3c1622cdbc72429ccc154791f21223b4 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 6 Dec 2024 17:28:15 -0800 Subject: [PATCH 5/5] Switch from Gerald to use native CODEOWNERS file (#1968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: I would like to start using Dependabot to help keep some deps up to date. We already have Dependabot configured to update two Khan Academy lint packages, but currently the Gerald check fails because it needs access to an org-wide secret (which it doesn't have access to). The good news is that this repo doesn't really use Gerald very much (only two rules). These rules are easily re-created with the [`CODEOWNERS`](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file) file instead. Issue: "none" ## Test plan: We'll need to land this PR and watch the next few PRs and see if it tags the team correctly. The failure mode is just that we won't be tagged as a team. Author: jeremywiebe Reviewers: SonicScrewdriver Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x) Pull Request URL: https://github.com/Khan/perseus/pull/1968 --- .github/CODEOWNERS | 5 +++ .github/REVIEWERS | 55 --------------------------------- .github/workflows/gerald-pr.yml | 13 -------- 3 files changed, 5 insertions(+), 68 deletions(-) create mode 100644 .github/CODEOWNERS delete mode 100644 .github/REVIEWERS delete mode 100644 .github/workflows/gerald-pr.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..dc289313c7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Khan/perseus is tagged for all PRs +@Khan/perseus + +# AI Utils are owned by Tutor Platform +widget-ai-utils/* @Khan/tut diff --git a/.github/REVIEWERS b/.github/REVIEWERS deleted file mode 100644 index 91068019d2..0000000000 --- a/.github/REVIEWERS +++ /dev/null @@ -1,55 +0,0 @@ -[ REVIEWER RULES] - -Be sure to read through https://khanacademy.atlassian.net/wiki/spaces/FRONTEND/pages/598278672/Gerald+Documentation before adding any rules! - -Examples: - -# This rule will request @owner1 for review on changes to all files. This rule will also request @owner2 for a blocking review. -# **/* @owner1 @owner2! - -# This rule will request @owner1 and @Org/team1 for review on changes to all .js files -# **/*.js @owner1 @Org/team1 - -# This rule will request @owner1 and @owner2 for review on changes to all files in the src/ directory. It will not match files in nested directories, such as src/about/about.js -# src/* @owner1 @owner2 - -# This rule will request @owner1 and @owner2 for review on changes to all files in the src/ directory, recursively. In contrast to the rule above, it WILL match src/about/about.js -# src/** @owner1 @owner2 - -# This rule will request @owner1 for review on changes to all files that have the word "gerald" in its name -# **/*gerald* @owner1 - -# The following rules will both request @owner1 for review on changes to any file that ends with .js, .txt, or .yml -# **/*.(js|txt|yml) @owner1 # This is in the style of Regex groups (https://www.regular-expressions.info/brackets.html) -# **/*.{js,txt,yml} @owner1 # This is in the style of Bash brace expansions (https://github.com/micromatch/braces) - -# This rule will request @owner1 for review on changes made to main.js or main.test.js. Read more about extended globbing: https://github.com/micromatch/micromatch#extglobs -# main?(.test).js @owner1 - -# This rule will request @owner1 for review on changes made to file-1, file-2, and file-3. -# file-[1-5] @owner1 # This is in the style of Regex character glasses (https://github.com/micromatch/micromatch#regex-character-classes) - -# This rule will request @owner1 for review on changes made to file-0, file-2, file-3, ..., file-9. -# file-[[:digit:]] @owner1 # This uses POSIX character classes (https://github.com/micromatch/picomatch#posix-brackets) - -Regex Examples: - -# This rule will request @owner1 for review on changes that include the word "gerald" -# "/gerald/ig" @owner1 - -# This rule will request @owner1 for review on changes that *add* the word "gerald" -# "/^\+.*gerald/igm" @owner1 - -# This rule will request @owner1 for review on changes that *remove* the word "gerald" -# "/^\-.*gerald/igm" @owner1 - -# This rule will request @owner1 for review on changes that *add OR remove* the word "gerald" -# "/^(\-|\+).*gerald/igm" @owner1 - -----Everything above this line will be ignored!---- -[ON PULL REQUEST] (DO NOT DELETE THIS LINE) - -**/* @Khan/perseus! - -# AI Utils are owned by Tutor Platform -widget-ai-utils/* @Khan/tut diff --git a/.github/workflows/gerald-pr.yml b/.github/workflows/gerald-pr.yml deleted file mode 100644 index fe157eaa35..0000000000 --- a/.github/workflows/gerald-pr.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Gerald - Notify and Request Reviewers On Pull Request -"on": - pull_request: - types: [opened, synchronize, reopened, ready_for_review, edited] - -jobs: - gerald: - runs-on: ubuntu-latest - steps: - - uses: Khan/actions@gerald-pr-v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - admin-token: ${{ secrets.KHAN_ACTIONS_BOT_TOKEN }}