Skip to content

Improve schema and user input type documentation#3357

Draft
jeremywiebe wants to merge 3 commits intomainfrom
jer/docs
Draft

Improve schema and user input type documentation#3357
jeremywiebe wants to merge 3 commits intomainfrom
jer/docs

Conversation

@jeremywiebe
Copy link
Collaborator

@jeremywiebe jeremywiebe commented Mar 14, 2026

Summary:

This PR is strictly comment changes.

We have decent coverage of comments in data-schema.ts, but they are not JSDoc comments, and so many tools don't display these comments with the type where it's used. This PR converts all relevant // ... comments to true JSDoc comments /** ... */.

We also add/enhance JSDoc comments for all user input, validation, and rubric types in validation.types.ts.

This should help human authors as they work with these types, but we anticipate it also helping AI agents to understand the types much better.

NOTE: The Schema Change check triggered but as you can see in the diff, it's only comment changes so it's a false report. Eventually we should probably filter comments out from that diff.

Issue: LEMS-3949

Test plan:

This is strictly documentation, so reading new comments would be helpful.

@jeremywiebe jeremywiebe self-assigned this Mar 14, 2026
@github-actions github-actions bot added the schema-change Attached to PRs when we detect Perseus Schema changes in it label Mar 14, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

🗄️ Schema Change: Changes Detected ⚠️

Usually this means you need to update the Go parser
that Content Platform maintains!!!

See this list of post-mortems for more information.

This PR contains critical changes to Perseus. Please review
the changes and note that you may need to coordinate
deployment of these changes with other teams at Khan Academy.

diff --unified /home/runner/work/_temp/branch-compare/base/schema.d.ts /home/runner/work/_temp/branch-compare/pr/schema.d.ts
--- /home/runner/work/_temp/branch-compare/base/schema.d.ts	2026-03-14 00:20:17.723416851 +0000
+++ /home/runner/work/_temp/branch-compare/pr/schema.d.ts	2026-03-14 00:20:06.385469908 +0000
@@ -153,6 +153,7 @@
   import type { KeypadKey } from "keypad";
   export type Coord = [x: number, y: number];
   export type Interval = [min: number, max: number];
+  /** Same name as Mafs */
   export type Vector2 = Coord;
   export type Range = Interval;
   export type Size = [width: number, height: number];
@@ -322,8 +323,11 @@
    * area.
    */
   export type PerseusItem = {
+    /** The details of the question being asked to the user. */
     question: PerseusRenderer;
+    /** A collection of hints to be offered to the user that support answering the question. */
     hints: Hint[];
+    /** Details about the tools the user might need to answer the question */
     answerArea: PerseusAnswerArea | null | undefined;
   };
   /**
@@ -332,7 +336,9 @@
    */
   export type PerseusArticle = PerseusRenderer | PerseusRenderer[];
   export type Version = {
+    /** The major part of the version */
     major: number;
+    /** The minor part of the version */
     minor: number;
   };
   export type PerseusRenderer = {
@@ -381,7 +387,9 @@
     placeholder?: boolean;
   };
   export type PerseusImageDetail = {
+    /** The width of the image */
     width: number;
+    /** the height of the image */
     height: number;
   };
   export const ItemExtras: readonly [
@@ -401,12 +409,35 @@
     Type extends string,
     Options extends Record<string, any>,
   > = {
+    /** The "type" of widget which will define what the Options field looks like */
     type: Type;
+    /** Whether this widget is displayed with the values and is immutable. For display only */
     static?: boolean;
+    /**
+     * Whether a widget is scored. Usually true except for IFrame widgets (deprecated).
+     * Default: true
+     */
     graded?: boolean;
+    /**
+     * The HTML alignment of the widget. "default" or "block". If the alignment is
+     * "default", it gets the default alignment from the widget logic, which can be
+     * various other alignments (e.g. "inline-block", "inline", etc).
+     */
     alignment?: string;
+    /**
+     * Options specific to the type field of the widget. See Perseus*WidgetOptions for
+     * more details
+     */
     options: Options;
+    /**
+     * Only used by interactive child widgets (line, point, etc) to identify the
+     * components
+     */
     key?: number | null;
+    /**
+     * The version of the widget data spec. Used to differentiate between newer and
+     * older content data.
+     */
     version?: Version;
   };
   export type CategorizerWidget = WidgetOptions<
@@ -526,12 +557,19 @@
    * A background image applied to various widgets.
    */
   export type PerseusImageBackground = {
+    /** The URL of the image */
     url?: string | null;
+    /** The width of the image */
     width?: number;
+    /** The height of the image */
     height?: number;
+    /** The top offset of the image */
     top?: number;
+    /** The left offset of the image */
     left?: number;
+    /** The scale of the image */
     scale?: number;
+    /** The bottom offset of the image */
     bottom?: number;
   };
   /**
@@ -544,34 +582,73 @@
   export type MarkingsType = "axes" | "graph" | "grid" | "none";
   export type AxisLabelLocation = "onAxis" | "alongEdge";
   export type PerseusCategorizerWidgetOptions = {
+    /**
+     * Translatable text; a list of items to categorize. e.g. ["banana",
+     * "yellow", "apple", "purple", "shirt"]
+     */
     items: string[];
+    /** Translatable text; a list of categories. e.g. ["fruits", "colors", "clothing"] */
     categories: string[];
+    /** Whether the items should be randomized */
     randomizeItems: boolean;
+    /** Whether this widget is displayed with the results and immutable */
     static: boolean;
+    /**
+     * The correct answers where index relates to the items and value relates to
+     * the category. e.g. [0, 1, 0, 1, 2]
+     */
     values: number[];
+    /** Whether we should highlight i18n linter errors found on this widget */
     highlightLint?: boolean;
   };
   export type PerseusDefinitionWidgetOptions = {
+    /** Translatable text; the word to define. e.g. "vertex" */
     togglePrompt: string;
+    /** Translatable text; the definition of the word. e.g. "where 2 rays connect" */
     definition: string;
+    /** Always false. Not used for this widget */
     static: boolean;
   };
   export type PerseusDropdownWidgetOptions = {
+    /** A list of choices for the dropdown */
     choices: PerseusDropdownChoice[];
+    /** Translatable Text; placeholder text for a dropdown. e.g. "Please select a fruit" */
     placeholder: string;
+    /** Always false. Not used for this widget */
     static: boolean;
+    /** Translatable Text; visible label for the dropdown */
     visibleLabel?: string;
+    /** Translatable Text; aria label that screen readers will read */
     ariaLabel?: string;
   };
   export type PerseusDropdownChoice = {
+    /** Translatable text; The text for the option. e.g. "Banana" or "Orange" */
     content: string;
+    /** Whether this is the correct option or not */
     correct: boolean;
   };
   export type PerseusExplanationWidgetOptions = {
+    /**
+     * Translatable Text; The clickable text to expand an explanation.
+     * e.g. "What is an apple?"
+     */
     showPrompt: string;
+    /**
+     * Translatable Text; The clickable text to hide an explanation.
+     * e.g. "Thanks. I got it!"
+     */
     hidePrompt: string;
+    /**
+     * Translatable Markdown; The explanation that is shown when showPrompt is
+     * clicked. e.g. "An apple is a tasty fruit."
+     */
     explanation: string;
+    /**
+     * explanation fields can embed widgets. When they do, the details of the
+     * widgets are here.
+     */
     widgets: PerseusWidgetsMap;
+    /** Always false. Not used for this widget */
     static: boolean;
   };
   export type LegacyButtonSets = Array<
@@ -585,13 +662,28 @@
     | "scientific"
   >;
   export type PerseusExpressionWidgetOptions = {
+    /** The expression forms the answer may come in */
     answerForms: PerseusExpressionAnswerForm[];
     buttonSets: LegacyButtonSets;
+    /** Variables that can be used as functions. Default: ["f", "g", "h"] */
     functions: string[];
+    /** Use x for rendering multiplication instead of a center dot. */
     times: boolean;
+    /**
+     * What extra keys need to be displayed on the keypad so that the question
+     * can be answerable without a keyboard (ie mobile)
+     */
     extraKeys?: KeypadKey[];
+    /** Visible label associated with the MathQuill field */
     visibleLabel?: string;
+    /** Aria label for screen readers attached to MathQuill field */
     ariaLabel?: string;
+    /**
+     * Controls when buttons for special characters are visible when using a
+     * desktop browser. Defaults to "focused".
+     * NOTE: This isn't listed in perseus-format.js or perseus_data.go, but
+     * appears in item data in the datastore.
+     */
     buttonsVisible?: "always" | "never" | "focused";
   };
   export const PerseusExpressionAnswerFormConsidered: readonly [
@@ -600,63 +692,149 @@
     "ungraded",
   ];
   export type PerseusExpressionAnswerForm = {
+    /** The TeX form of the expression. e.g. "x\\cdot3=y" */
     value: string;
+    /** The Answer expression must have the same form */
     form: boolean;
+    /** The answer expression must be fully expanded and simplified */
     simplify: boolean;
+    /** Whether the form is considered "correct", "wrong", or "ungraded" */
     considered: (typeof PerseusExpressionAnswerFormConsidered)[number];
+    /**
+     * A key to identify the answer form in a list.
+     * NOTE: perseus-format.js says this is required even though it isn't necessary.
+     */
     key?: string;
   };
   export type PerseusGradedGroupWidgetOptions = {
+    /** Translatable Text; A title to be displayed for the group. */
     title: string;
+    /** Not used in Perseus (but is set in (en, pt) production data) */
     hasHint?: boolean | null | undefined;
+    /** A section to define hints for the group. */
     hint?: PerseusRenderer | null | undefined;
+    /** Translatable Markdown. May include widgets and images embedded. */
     content: string;
+    /** See PerseusRenderer.widgets */
     widgets: PerseusWidgetsMap;
+    /** Not used in Perseus */
     widgetEnabled?: boolean | null | undefined;
+    /** Not used in Perseus */
     immutableWidgets?: boolean | null | undefined;
+    /** See PerseusRenderer.images */
     images: {
       [key: string]: PerseusImageDetail;
     };
   };
   export type PerseusGradedGroupSetWidgetOptions = {
+    /** A list of Widget Groups */
     gradedGroups: PerseusGradedGroupWidgetOptions[];
   };
+  /** A 2D coordinate range: x-axis [min, max] and y-axis [min, max]. */
   export type GraphRange = [
     x: [min: number, max: number],
     y: [min: number, max: number],
   ];
+  /**
+   * The state of the grapher widget's plotted function, discriminated by
+   * function type. Used as both the learner's user input and the rubric's
+   * correct answer.
+   */
   export type GrapherAnswerTypes =
     | {
+        /**
+         * A V-shaped graph defined by its vertex.
+         */
         type: "absolute_value";
+        /**
+         * The vertex and a second point defining the V-shape. If null,
+         * the graph is not gradable and all answers score as invalid.
+         */
         coords: null | [vertex: Coord, secondPoint: Coord];
       }
     | {
+        /**
+         * A curve of the form y = a·bˣ approaching a horizontal (or
+         * vertical) asymptote.
+         */
         type: "exponential";
+        /**
+         * Two points along the asymptote line. Usually (always?) a
+         * horizontal or vertical line.
+         */
         asymptote: [Coord, Coord];
+        /**
+         * Two points along the exponential curve. One end of the curve
+         * trends towards the asymptote. If null, the graph is not
+         * gradable and all answers score as invalid.
+         */
         coords: null | [Coord, Coord];
       }
     | {
+        /**
+         * A straight line of the form y = mx + b.
+         */
         type: "linear";
+        /**
+         * Two points along the straight line. If null, the graph is not
+         * gradable and all answers score as invalid.
+         */
         coords: null | [Coord, Coord];
       }
     | {
+        /**
+         * A curve of the form y = a·log_b(x) approaching a vertical
+         * asymptote.
+         */
         type: "logarithm";
+        /** Two points along the asymptote line. */
         asymptote: [Coord, Coord];
+        /**
+         * Two points along the logarithmic curve. One end of the curve
+         * trends towards the asymptote. If null, the graph is not
+         * gradable and all answers score as invalid.
+         */
         coords: null | [Coord, Coord];
       }
     | {
+        /**
+         * A parabola of the form y = ax² + bx + c.
+         */
         type: "quadratic";
+        /**
+         * The vertex and a second point defining the parabola. If null,
+         * the graph is not gradable and all answers score as invalid.
+         */
         coords: null | [vertex: Coord, secondPoint: Coord];
       }
     | {
+        /**
+         * A periodic wave of the form y = a·sin(bx + c) + d.
+         */
         type: "sinusoid";
+        /**
+         * Two points on the same slope of the sinusoid. If null, the
+         * graph is not gradable and all answers score as invalid.
+         */
         coords: null | [Coord, Coord];
       }
     | {
+        /**
+         * A periodic curve of the form y = a·tan(bx + c) + d.
+         */
         type: "tangent";
+        /**
+         * Two points on the same slope of the tangent curve. If null,
+         * the graph is not gradable and all answers score as invalid.
+         */
         coords: null | [Coord, Coord];
       };
+  /**
+   * Options for the Grapher widget. Defines the available function
+   * types, the correct answer, and the visual graph configuration.
+   */
   export type PerseusGrapherWidgetOptions = {
+    /** The set of function types the learner can choose from when plotting. */
     availableTypes: Array<
       | "absolute_value"
       | "exponential"
@@ -666,41 +844,78 @@
       | "sinusoid"
       | "tangent"
     >;
+    /** The correct answer; used to score the learner's plotted function. */
     correct: GrapherAnswerTypes;
+    /** Visual configuration for the coordinate plane. */
     graph: {
+      /** An optional background image displayed behind the graph. */
       backgroundImage: {
+        /** Vertical offset from the bottom of the graph in pixels. */
         bottom?: number;
+        /** Height of the image in pixels. */
         height?: number;
+        /** Horizontal offset from the left edge of the graph in pixels. */
         left?: number;
+        /** Scale factor applied to the image. */
         scale?: number;
+        /** URL of the background image, or null/undefined if none. */
         url?: string | null | undefined;
+        /** Width of the image in pixels. */
         width?: number;
       };
+      /** The [width, height] of the graph canvas in pixels. */
       box?: [number, number];
+      /** Which graph settings are editable in the editor UI. */
       editableSettings?: Array<"graph" | "snap" | "image" | "measure">;
+      /** The [x, y] spacing between grid lines. */
       gridStep?: [number, number];
+      /** The [x-axis, y-axis] labels. */
       labels: [string, string];
+      /** Which markings to show on the graph (axes, grid, graph, or none). */
       markings: MarkingsType;
+      /** The visible [x-range, y-range] of the coordinate plane. */
       range: GraphRange;
+      /** The label for the ruler overlay (currently always empty string). */
       rulerLabel: "";
+      /** The number of tick marks on the ruler overlay. */
       rulerTicks: number;
+      /** When true, a protractor overlay is shown on the graph. */
       showProtractor?: boolean;
+      /** When true, a ruler overlay is shown on the graph. */
       showRuler?: boolean;
+      /** When true, coordinate tooltips are shown on hover. */
       showTooltips?: boolean;
+      /** The [x, y] snap increment for interactive elements. */
       snapStep?: [number, number];
+      /** The [x, y] distance between labeled tick marks. */
       step: [number, number];
+      /**
+       * Whether the graph configuration is valid. Can be false or an
+       * error message string.
+       */
       valid?: boolean | string;
     };
   };
   export type PerseusGroupWidgetOptions = PerseusRenderer;
   export type PerseusImageWidgetOptions = {
+    /** Translatable Markdown; Text to be shown for the title of the image */
     title?: string;
+    /** Translatable Markdown; Text to be shown in the caption section of an image */
     caption?: string;
+    /** Translatable Text; The alt text to be shown in the img.alt attribute */
     alt?: string;
+    /** Translatable Markdown; Text to be shown as the long description of an image */
     longDescription?: string;
+    /**
+     * When true, standalone image will be rendered with alt="" and without any alt
+     * text, caption, title, or long description.
+     */
     decorative?: boolean;
+    /** The image details for the image to be displayed */
     backgroundImage: PerseusImageBackground;
+    /** The size scale of the image */
     scale?: number;
+    /** Always false. Not used for this widget */
     static?: boolean;
     /** @deprecated - labels were removed from the image widget in 2017 */
     labels?: Array<PerseusImageLabel>;
@@ -710,19 +925,33 @@
     box?: Size;
   };
   export type PerseusImageLabel = {
+    /** Translatable Text; The content of the label to display */
     content: string;
+    /** The visual alignment of the label. default: "center" */
     alignment: string;
+    /** The point on the image to display the label */
     coordinates: number[];
   };
   export type PerseusInteractiveGraphWidgetOptions = {
+    /**
+     * Where the little black axis lines & labels (ticks) should render. Also
+     * known as the tick step. default [1, 1]
+     */
     step: [number, number];
+    /** Where the grid lines on the graph will render. default [1, 1] */
     gridStep?: [x: number, y: number];
+    /**
+     * Where the graph points will lock to when they are dragged.
+     * default [0.5, 0.5]
+     */
     snapStep?: [x: number, y: number];
+    /** An optional image to use in the background */
     backgroundImage?: PerseusImageBackground;
     /**
      * The type of markings to display on the graph.
      */
     markings: MarkingsType;
+    /** How to label the X and Y axis. default: ["x", "y"] */
     labels?: string[];
     /**
      * Specifies the location of the labels on the graph.  default: "onAxis".
@@ -731,7 +960,9 @@
      *    The y label is rotated. Typically used when the range min is near 0 with longer labels.
      */
     labelLocation?: AxisLabelLocation;
+    /** Which sides of the graph are bounded (removed axis arrows). */
     showAxisArrows: ShowAxisArrows;
+    /** Whether to show the Protractor tool overlayed on top of the graph */
     showProtractor: boolean;
     /**
      * Whether to show the Ruler tool overlayed on top of the graph.
@@ -740,6 +971,7 @@
      * features, since it may appear in production data.
      */
     showRuler?: boolean;
+    /** Whether to show tooltips on the graph */
     showTooltips?: boolean;
     /**
      * The unit to show on the ruler.  e.g. "mm", "cm",  "m", "km", "in", "ft",
@@ -757,11 +989,23 @@
      * features, since it may appear in production data.
      */
     rulerTicks?: number;
+    /**
+     * The X and Y coordinate ranges for the view of the graph.
+     * default: [[-10, 10], [-10, 10]]
+     */
     range: GraphRange;
+    /** The type of graph */
     graph: PerseusGraphType;
+    /** The correct kind of graph, if being used to select function type */
     correct: PerseusGraphType;
+    /**
+     * Shapes (points, chords, etc) displayed on the graph that cannot be moved
+     * by the user.
+     */
     lockedFigures: LockedFigure[];
+    /** Aria label that applies to the entire graph. */
     fullGraphAriaLabel?: string;
+    /** Aria description that applies to the entire graph. */
     fullGraphAriaDescription?: string;
   };
   export const lockedFigureColorNames: readonly [
@@ -861,9 +1105,11 @@
     labels: LockedLabelType[];
     ariaLabel?: string;
   };
+  /** Not associated with a specific figure */
   export type LockedLabelType = {
     type: "label";
     coord: Coord;
+    /** TeX-supported string */
     text: string;
     color: LockedFigureColor;
     size: "small" | "medium" | "large";
@@ -882,18 +1128,26 @@
     | PerseusGraphTypeSinusoid;
   export type PerseusGraphTypeAngle = {
     type: "angle";
+    /** Whether to show the angle measurements. default: false */
     showAngles?: boolean;
+    /** Allow Reflex Angles if an "angle" type. default: true */
     allowReflexAngles?: boolean;
+    /** The angle offset in degrees if an "angle" type. default: 0 */
     angleOffsetDeg?: number;
+    /** Snap to degree increments if an "angle" type. default: 1 */
     snapDegrees?: number;
+    /** How to match the answer. If missing, defaults to exact matching. */
     match?: "congruent";
+    /** must have 3 coords - ie [Coord, Coord, Coord] */
     coords?: [Coord, Coord, Coord];
+    /** The initial coordinates the graph renders with. */
     startCoords?: [Coord, Coord, Coord];
   };
   export type PerseusGraphTypeCircle = {
     type: "circle";
     center?: Coord;
     radius?: number;
+    /** The initial coordinates the graph renders with. */
     startCoords?: {
       center: Coord;
       radius: number;
@@ -901,12 +1155,16 @@
   };
   export type PerseusGraphTypeLinear = {
     type: "linear";
+    /** expects 2 coords */
     coords?: CollinearTuple | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: CollinearTuple;
   };
   export type PerseusGraphTypeLinearSystem = {
     type: "linear-system";
+    /** expects 2 sets of 2 coords */
     coords?: CollinearTuple[] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: CollinearTuple[];
   };
   export type PerseusGraphTypeNone = {
@@ -914,40 +1172,64 @@
   };
   export type PerseusGraphTypePoint = {
     type: "point";
+    /**
+     * The number of points if a "point" type. default: 1. "unlimited" if no
+     * limit
+     */
     numPoints?: number | "unlimited";
     coords?: Coord[] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: Coord[];
+    /** Used instead of `coords` in some old graphs that have only one point. */
     coord?: Coord;
   };
   export type PerseusGraphTypePolygon = {
     type: "polygon";
+    /** The number of sides. default: 3. "unlimited" if no limit */
     numSides?: number | "unlimited";
+    /** Whether to show the angle measurements. default: false */
     showAngles?: boolean;
+    /** Whether to show side measurements. default: false */
     showSides?: boolean;
+    /** How to snap points. e.g. "grid", "angles", or "sides". default: grid */
     snapTo?: "grid" | "angles" | "sides";
+    /** How to match the answer. If missing, defaults to exact matching. */
     match?: "similar" | "congruent" | "approx" | "exact";
     coords?: Coord[] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: Coord[];
   };
   export type PerseusGraphTypeQuadratic = {
     type: "quadratic";
+    /** expects a list of 3 coords */
     coords?: [Coord, Coord, Coord] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: [Coord, Coord, Coord];
   };
   export type PerseusGraphTypeSegment = {
     type: "segment";
+    /** The number of segments if a "segment" type. default: 1. Max: 6 */
     numSegments?: number;
+    /**
+     * Expects a list of Coord tuples. Length should match the `numSegments`
+     * value.
+     */
     coords?: CollinearTuple[] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: CollinearTuple[];
   };
   export type PerseusGraphTypeSinusoid = {
     type: "sinusoid";
+    /** Expects a list of 2 Coords */
     coords?: Coord[] | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: Coord[];
   };
   export type PerseusGraphTypeRay = {
     type: "ray";
+    /** Expects a list of 2 Coords */
     coords?: CollinearTuple | null;
+    /** The initial coordinates the graph renders with. */
     startCoords?: CollinearTuple;
   };
   type AngleGraphCorrect = {
@@ -1010,46 +1292,109 @@
     | SegmentGraphCorrect
     | SinusoidGraphCorrect;
   export type PerseusLabelImageWidgetOptions = {
+    /** Translatable Text; TeX representation of choices */
     choices: string[];
+    /** The URL of the image */
     imageUrl: string;
+    /** Translatable Text; To show up in the img.alt attribute */
     imageAlt: string;
+    /** The height of the image */
     imageHeight: number;
+    /** The width of the image */
     imageWidth: number;
+    /** A list of markers to display on the image */
     markers: PerseusLabelImageMarker[];
+    /** Do not display answer choices in instructions */
     hideChoicesFromInstructions: boolean;
+    /** Allow multiple answers per marker */
     multipleAnswers: boolean;
+    /** Always false. Not used for this widget */
     static: boolean;
   };
   export type PerseusLabelImageMarker = {
+    /**
+     * A list of correct answers for this marker. Often only one but can have
+     * multiple
+     */
     answers: string[];
+    /**
+     * Translatable Text; The text to show for the marker. Not displayed
+     * directly to the user
+     */
     label: string;
+    /** X Coordinate location of the marker on the image */
     x: number;
+    /** Y Coordinate location of the marker on the image */
     y: number;
   };
   export type PerseusMatcherWidgetOptions = {
+    /**
+     * Translatable Text; Labels to adorn the headings for the columns. Only 2
+     * values [left, right]. e.g. ["Concepts", "Things"]
+     */
     labels: string[];
+    /**
+     * Translatable Text; Static concepts to show in the left column.
+     * e.g. ["Fruit", "Color", "Clothes"]
+     */
     left: string[];
+    /**
+     * Translatable Markup; Values that represent the concepts to be correlated
+     * with the concepts. e.g. ["Red", "Shirt", "Banana"]
+     */
     right: string[];
+    /**
+     * Order of the matched pairs matters. With this option enabled, only the
+     * order provided above will be treated as correct. This is useful when
+     * ordering is significant, such as in the context of a proof. If disabled,
+     * pairwise matching is sufficient. To make this clear, the left column
+     * becomes fixed in the provided order and only the cards in the right
+     * column can be moved.
+     */
     orderMatters: boolean;
+    /** Adds padding to the rows. Padding is good for text, but not needed for images. */
     padding: boolean;
   };
   export type PerseusMatrixWidgetAnswers = number[][];
   export type PerseusMatrixWidgetOptions = {
+    /** Translatable Text; Shown before the matrix */
     prefix?: string | undefined;
+    /** Translatable Text; Shown after the matrix */
     suffix?: string | undefined;
+    /**
+     * A data matrix representing the "correct" answers to be entered into the
+     * matrix
+     */
     answers: PerseusMatrixWidgetAnswers;
+    /** The coordinate location of the cursor position at start. default: [0, 0] */
     cursorPosition?: number[] | undefined;
+    /**
+     * The coordinate size of the matrix. Only supports 2-dimensional matrix.
+     * default: [3, 3]
+     */
     matrixBoardSize: number[];
+    /**
+     * Whether this is meant to statically display the answers (true) or be
+     * used as an input field, graded against the answers
+     */
     static?: boolean | undefined;
   };
   export type PerseusMeasurerWidgetOptions = {
+    /** The image that the user is meant to measure */
     image: PerseusImageBackground;
+    /** Whether to show the Protractor tool overlayed on top of the image */
     showProtractor: boolean;
+    /** Whether to show the Ruler tool overlayed on top of the image */
     showRuler: boolean;
+    /** The unit to show on the ruler. e.g. "mm", "cm", "m", "km", "in", "ft", "yd", "mi" */
     rulerLabel: string;
+    /** How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16 */
     rulerTicks: number;
+    /** The number of image pixels per unit (label) */
     rulerPixels: number;
+    /** The number of units to display on the ruler */
     rulerLength: number;
+    /** Containing area [width, height] */
     box: [number, number];
   };
   export type MathFormat =
@@ -1077,44 +1422,137 @@
     | "enforced"
     | "optional";
   export type PerseusNumericInputWidgetOptions = {
+    /** A list of all the possible correct and incorrect answers */
     answers: PerseusNumericInputAnswer[];
+    /**
+     * Translatable Text; Text to describe this input. This will be shown to
+     * users using screenreaders.
+     */
     labelText?: string | undefined;
+    /**
+     * Use size "Normal" for all text boxes, unless there are multiple text
+     * boxes in one line and the answer area is too narrow to fit them.
+     * Options: "normal" or "small"
+     */
     size: string;
+    /**
+     * A coefficient style number allows the student to use - for -1 and an
+     * empty string to mean 1.
+     */
     coefficient: boolean;
+    /** Whether to right-align the text or not */
     rightAlign?: boolean;
+    /** Always false. Not used for this widget */
     static: boolean;
   };
   export type PerseusNumericInputAnswer = {
+    /**
+     * Translatable Display; A description for why this answer is correct,
+     * wrong, or ungraded
+     */
     message: string;
+    /** The expected answer */
     value?: number | null;
+    /** Whether this answer is "correct", "wrong", or "ungraded" */
     status: string;
+    /**
+     * The forms available for this answer. Options: "integer", "decimal",
+     * "proper", "improper", "mixed", or "pi"
+     */
     answerForms?: MathFormat[];
+    /**
+     * Whether we should check the answer strictly against the configured
+     * answerForms (strict = true) or include the set of default answerForms
+     * (strict = false).
+     */
     strict: boolean;
+    /** A range of error +/- the value */
     maxError?: number | null;
+    /** Unsimplified answers are Ungraded, Accepted, or Wrong. */
     simplify: PerseusNumericInputSimplify;
   };
   export type PerseusNumberLineWidgetOptions = {
+    /**
+     * The position of the endpoints of the number line. Setting the range
+     * constrains the position of the answer and the labels.
+     */
     range: number[];
+    /**
+     * This controls the position of the left / right labels. By default, the
+     * labels are set by the range. Note: Ensure that the labels line up with
+     * the tick marks, or it may be confusing for users.
+     */
     labelRange: Array<number | null>;
+    /**
+     * This controls the styling of the labels for the two main labels as well
+     * as all the tick mark labels, if applicable. Options: "decimal",
+     * "improper", "mixed", "non-reduced"
+     */
     labelStyle: string;
+    /** Show label ticks */
     labelTicks: boolean;
+    /** Show tick controller */
     isTickCtrl: boolean;
     isInequality: boolean;
+    /** The range of divisions within the line */
     divisionRange: number[];
+    /**
+     * This controls the number (and position) of the tick marks. The number of
+     * divisions is constrained to the division range. Note: The user will be
+     * able to specify the number of divisions in a number input.
+     */
     numDivisions?: number | null;
+    /**
+     * This determines the number of different places the point will snap
+     * between two adjacent tick marks. Note: Ensure the required number of
+     * snap increments is provided to answer the question.
+     */
     snapDivisions: number;
+    /**
+     * This controls the number (and position) of the tick marks; you can
+     * either set the number of divisions (2 divisions would split the entire
+     * range in two halves), or the tick step (the distance between ticks) and
+     * the other value will be updated accordingly. Note: There is no check to
+     * see if labels coordinate with the tick marks, which may be confusing for
+     * users if the blue labels and black ticks are off-step.
+     */
     tickStep?: number | null;
+    /**
+     * The answer to a NumberLine widget is a set of real numbers. `correctRel`
+     * expresses the relationship between the numbers in that set and the value
+     * of `correctX`.
+     */
     correctRel?: "eq" | "lt" | "gt" | "le" | "ge";
+    /**
+     * This is the correct answer. The answer is validated (as right or wrong)
+     * by using only the end position of the point and the relation
+     * (=, &lt;, &gt;, ≤, ≥).
+     */
     correctX: number | null;
+    /** This controls the initial position of the point along the number line */
     initialX?: number | null;
+    /** Show tooltips */
     showTooltips?: boolean;
+    /** When true, the answer is displayed and is immutable */
     static: boolean;
   };
   export type PerseusOrdererWidgetOptions = {
+    /**
+     * All of the options available to the user. Place the cards in the correct
+     * order. The same card can be used more than once in the answer but will
+     * only be displayed once at the top of a stack of identical cards.
+     */
     options: PerseusRenderer[];
+    /** The correct order of the options */
     correctOptions: PerseusRenderer[];
+    /** Cards that are not part of the answer */
     otherOptions: PerseusRenderer[];
+    /** "normal" for text options. "auto" for image options. */
     height: "normal" | "auto";
+    /**
+     * Use the "horizontal" layout for short text and small images. The
+     * "vertical" layout is best for longer text (e.g. proofs).
+     */
     layout: "horizontal" | "vertical";
   };
   export const plotterPlotTypes: readonly [
@@ -1126,231 +1564,423 @@
   ];
   export type PlotType = (typeof plotterPlotTypes)[number];
   export type PerseusPlotterWidgetOptions = {
+    /** Translatable Text; The Axis labels. e.g. ["X Label", "Y Label"] */
     labels: string[];
+    /**
+     * Translatable Text; Categories to display along the X axis.
+     * e.g. [">0", ">6", ">12", ">18"]
+     */
     categories: string[];
+    /** The type of the graph. options "bar", "line", "pic", "histogram", "dotplot" */
     type: PlotType;
+    /** The maximum Y tick to display in the graph */
     maxY: number;
+    /** The scale of the Y Axis */
     scaleY: number;
+    /**
+     * Which ticks to display the labels for. For instance, setting this to "4"
+     * will only show every 4th label (plus the last one)
+     */
     labelInterval?: number | null;
+    /**
+     * Creates the specified number of divisions between the horizontal lines.
+     * Fewer snaps between lines makes the graph easier for the student to
+     * create correctly.
+     */
     snapsPerLine: number;
+    /** The Y values the graph should start with */
     starting: number[];
+    /** The Y values that represent the correct answer expected */
     correct: number[];
+    /** A picture to represent items in a graph. */
     picUrl?: string | null;
+    /** @deprecated */
     picSize?: number | null;
+    /** @deprecated */
     picBoxHeight?: number | null;
+    /** @deprecated */
     plotDimensions: number[];
   };
   export type PerseusRadioWidgetOptions = {
+    /** The choices provided to the user. */
     choices: PerseusRadioChoice[];
+    /** Does this have a "none of the above" option? */
     hasNoneOfTheAbove?: boolean;
+    /** If multipleSelect is enabled, specify the number expected to be correct. */
     countChoices?: boolean;
+    /**
+     * How many of the choices are correct, which is conditionally used to tell
+     * learners ahead of time how many options they'll need.
+     */
     numCorrect?: number;
+    /** Randomize the order of the options or keep them as defined */
     randomize?: boolean;
+    /** Does this set allow for multiple selections to be correct? */
     multipleSelect?: boolean;
+    /** @deprecated */
     deselectEnabled?: boolean;
   };
   export type PerseusRadioChoice = {
+    /** Translatable Markdown; The label for this choice */
     content: string;
     /**
      * An opaque string that uniquely identifies this choice within
      * the radio widget. The format of this ID is subject to change.
      */
     id: string;
+    /** Translatable Markdown; Rationale to give the user when they get it wrong */
     rationale?: string;
+    /** Whether this option is a correct answer or not */
     correct?: boolean;
+    /** If this is none of the above, override the content with "None of the above" */
     isNoneOfTheAbove?: boolean;
   };
   export type PerseusSorterWidgetOptions = {
+    /**
+     * Translatable Text; The correct answer (in the correct order). The user
+     * will see the cards in a randomized order.
+     */
     correct: string[];
+    /**
+     * Adds padding to the options. Padding is good for text but not needed
+     * for images
+     */
     padding: boolean;
+    /**
+     * Use the "horizontal" layout for short text and small images. The
+     * "vertical" layout is best for longer text and larger images.
+     */
     layout: "horizontal" | "vertical";
   };
   export type PerseusTableWidgetOptions = {
+    /** Translatable Text; A list of column headers */
     headers: string[];
+    /** The number of rows to display */
     rows: number;
+    /** The number of columns to display */
     columns: number;
+    /** Translatable Text; A 2-dimensional array of text to populate the table with */
     answers: string[][];
   };
   export type PerseusInteractionWidgetOptions = {
+    /** The definition of the graph */
     graph: PerseusInteractionGraph;
+    /** The elements of the graph */
     elements: PerseusInteractionElement[];
+    /** Always false. Not used for this widget */
     static: boolean;
   };
   export type PerseusInteractionGraph = {
+    /** "canvas", "graph" */
     editableSettings?: Array<"canvas" | "graph">;
+    /** The Grid Canvas size. e.g. [400, 140] */
     box: Size;
+    /** The Axis labels. e.g. ["x", "y"] */
     labels: string[];
+    /** The Axis ranges. e.g. [[-10, 10], [-10, 10]] */
     range: [Interval, Interval];
+    /** The steps in the grid. default [1, 1] */
     gridStep: [number, number];
     /**
      * The type of markings to display on the graph.
      */
     markings: MarkingsType;
+    /** The snap steps. default [0.5, 0.5] */
     snapStep?: [number, number];
+    /**
+     * Whether the grid is valid or not. Do the numbers all make sense?
+     * NOTE(jeremy) The editor for this widget sometimes stores the graph
+     * editor validation error message into this field. It seems innocuous
+     * because it looks like many of these usages don't actually use the graph
+     * at all.
+     */
     valid?: boolean | string;
+    /** An optional background image to use */
     backgroundImage?: PerseusImageBackground;
+    /** Whether to show the Protractor tool overlayed on top of the graph */
     showProtractor?: boolean;
+    /** Whether to show the Ruler tool overlayed on top of the graph */
     showRuler?: boolean;
+    /** The unit to show on the ruler. e.g. "mm", "cm", "m", "km", "in", "ft", "yd", "mi" */
     rulerLabel?: string;
+    /** How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16 */
     rulerTicks?: number;
+    /**
+     * This controls the number (and position) of the tick marks for the X and
+     * Y axis. e.g. [1, 1]
+     */
     tickStep: [number, number];
   };
   export type PerseusInteractionElement =
     | {
         type: "function";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionFunctionElementOptions;
       }
     | {
         type: "label";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionLabelElementOptions;
       }
     | {
         type: "line";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionLineElementOptions;
       }
     | {
         type: "movable-line";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionMovableLineElementOptions;
       }
     | {
         type: "movable-point";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionMovablePointElementOptions;
       }
     | {
         type: "parametric";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionParametricElementOptions;
       }
     | {
         type: "point";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionPointElementOptions;
       }
     | {
         type: "rectangle";
+        /** An identifier for the element */
         key: string;
         options: PerseusInteractionRectangleElementOptions;
       };
   export type PerseusInteractionFunctionElementOptions = {
+    /** The definition of the function to draw on the graph. e.g "x^2 + 1" */
     value: string;
+    /** The name of the function like f(n). default: "f" */
     funcName: string;
+    /** The range of points to start plotting */
     rangeMin: string;
+    /** The range of points to end plotting */
     rangeMax: string;
+    /** The color of the stroke. e.g. #6495ED */
     color: string;
+    /** If the function stroke has a dash, what is it? options: "", "-", "- ", ".", ". " */
     strokeDasharray: string;
+    /** The thickness of the stroke */
     strokeWidth: number;
   };
   export type PerseusInteractionLabelElementOptions = {
+    /** Translatable Text; the content of the label */
     label: string;
+    /** The color of the label. e.g. "red" */
     color: string;
+    /** The X location of the label */
     coordX: string;
+    /** The Y location of the label */
     coordY: string;
   };
   export type PerseusInteractionLineElementOptions = {
+    /** A color code for the line segment. e.g. "#FFOOAF" */
     color: string;
+    /** The start of the line segment (X) */
     startX: string;
+    /** The start of the line segment (Y) */
     startY: string;
+    /** The end of the line segment (X) */
     endX: string;
+    /** The end of the line segment (Y) */
     endY: string;
+    /** If the line stroke has a dash, what is it? options: "", "-", "- ", ".", ". " */
     strokeDasharray: string;
+    /** The thickness of the line */
     strokeWidth: number;
+    /** Does the line have an arrow point to it? options: "", "->" */
     arrows: string;
   };
   export type PerseusInteractionMovableLineElementOptions = {
+    /** The start of the line segment (X) */
     startX: string;
+    /** The start of the line segment (Y) */
     startY: string;
+    /** Start updates (Xn, Yn) for n */
     startSubscript: number;
+    /** The end of the line segment (X) */
     endX: string;
+    /** The end of the line segment (Y) */
     endY: string;
+    /** End updates (Xm, Ym) for m */
     endSubscript: number;
+    /** How to constrain this line? options "none", "snap", "x", "y" */
     constraint: string;
+    /** The snap resolution when constraint is set to "snap" */
     snap: number;
+    /** The constraint function for when constraint is set to "x" or "y" */
     constraintFn: string;
+    /** The lowest possible X value */
     constraintXMin: string;
+    /** The highest possible X value */
     constraintXMax: string;
+    /** The lowest possible Y value */
     constraintYMin: string;
+    /** The highest possible Y value */
     constraintYMax: string;
   };
   export type PerseusInteractionMovablePointElementOptions = {
+    /** The X position of the point */
     startX: string;
+    /** The Y position of the point */
     startY: string;
+    /** Update (Xn, Yn) for n */
     varSubscript: number;
+    /** How to constrain this line? options "none", "snap", "x", "y" */
     constraint: string;
+    /** The snap resolution when constraint is set to "snap" */
     snap: number;
+    /** The constraint function for when constraint is set to "x" or "y" */
     constraintFn: string;
+    /** The lowest possible X value */
     constraintXMin: string;
+    /** The highest possible X value */
     constraintXMax: string;
+    /** The lowest possible Y value */
     constraintYMin: string;
+    /** The highest possible Y value */
     constraintYMax: string;
   };
   export type PerseusInteractionParametricElementOptions = {
+    /** The function for the X coordinate. e.g. "\\cos(t)" */
     x: string;
+    /** The function for the Y coordinate. e.g. "\\sin(t)" */
     y: string;
+    /** The range of points to start plotting */
     rangeMin: string;
+    /** The range of points to end plotting */
     rangeMax: string;
+    /** The color of the stroke. e.g. #6495ED */
     color: string;
+    /** If the function stroke has a dash, what is it? options: "", "-", "- ", ".", ". " */
     strokeDasharray: string;
+    /** The thickness of the stroke */
     strokeWidth: number;
   };
   export type PerseusInteractionPointElementOptions = {
+    /** The color of the point. e.g. "black" */
     color: string;
+    /** The X coordinate of the point */
     coordX: string;
+    /** The Y coordinate of the point */
     coordY: string;
   };
   export type PerseusInteractionRectangleElementOptions = {
+    /** The fill color. e.g. "#EDD19B" */
     color: string;
+    /** The lower left point X */
     coordX: string;
+    /** The lower left point Y */
     coordY: string;
+    /** The width of the rectangle */
     width: string;
+    /** The height of the rectangle */
     height: string;
   };
   export type PerseusCSProgramWidgetOptions = {
+    /** The ID of the CS program to embed */
     programID: string;
+    /** Deprecated. Always null and sometimes omitted entirely. */
     programType?: any;
+    /**
+     * Settings that you add here are available to the program as an object
+     * returned by Program.settings()
+     */
     settings: PerseusCSProgramSetting[];
+    /**
+     * If you show the editor, you should use the "full-width" alignment to
+     * make room for the width of the editor.
+     */
     showEditor: boolean;
+    /** Whether to show the execute buttons */
     showButtons: boolean;
+    /** The height of the widget */
     height: number;
     static: boolean;
   };
   export type PerseusCSProgramSetting = {
+    /** The name/key of the setting */
     name: string;
+    /** The value of the setting */
     value: string;
   };
   export type PerseusPythonProgramWidgetOptions = {
+    /** The ID of the Python program to embed */
     programID: string;
+    /** The height of the widget in pixels */
     height: number;
   };
+  /**
+   * This is an object instead of just a string because we think we'll want to
+   * add more fields in the future, like a weight, which would allow us to give
+   * partial credit and weight each criterion separately.
+   */
   export type PerseusFreeResponseWidgetScoringCriterion = {
+    /**
+     * An English-language description of how to score the response for this
+     * criterion.
+     */
     text: string;
   };
   export type PerseusFreeResponseWidgetOptions = {
+    /** Whether to allow the user to enter an unlimited number of characters. */
     allowUnlimitedCharacters: boolean;
+    /** The maximum number of characters that the user can enter. */
     characterLimit: number;
+    /**
+     * The placeholder text that will be displayed to the user in the text
+     * input field.
+     */
     placeholder: string;
+    /** The question text that will be displayed to the user. */
     question: string;
+    /**
+     * A list of scoring criteria for the free response question. This is a
+     * list of things the answer should contain to be considered correct.
+     */
     scoringCriteria: ReadonlyArray<PerseusFreeResponseWidgetScoringCriterion>;
   };
   export type PerseusIFrameWidgetOptions = {
+    /** A URL to display OR a CS Program ID */
     url: string;
+    /**
+     * Settings that you add here are available to the program as an object
+     * returned by Program.settings()
+     */
     settings?: PerseusCSProgramSetting[];
+    /** The width of the widget */
     width: number | string;
+    /** The height of the widget */
     height: number | string;
+    /** Whether to allow the IFrame to become full-screen (like a video) */
     allowFullScreen: boolean;
+    /** Whether to allow the iframe content to redirect the page */
     allowTopNavigation?: boolean;
+    /** Always false */
     static: boolean;
   };
   export type PerseusPhetSimulationWidgetOptions = {
+    /** A URL to display, must start with https://phet.colorado.edu/ */
     url: string;
+    /** Translatable Text; Description of the sim for Khanmigo and alt text */
     description: string;
   };
   export type PerseusVideoWidgetOptions = {
     location: string;
+    /** `static` is not used for the video widget. */
     static?: boolean;
   };
   export type PerseusInputNumberWidgetOptions = {

@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

Size Change: 0 B

Total Size: 486 kB

ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.8 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 5.96 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 11.8 kB
packages/perseus-core/dist/es/index.js 24.9 kB
packages/perseus-editor/dist/es/index.js 100 kB
packages/perseus-linter/dist/es/index.js 8.82 kB
packages/perseus-score/dist/es/index.js 9.26 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/index.js 187 kB
packages/perseus/dist/es/strings.js 7.47 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Contributor

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (13686c8) and published it to npm. You
can install it using the tag PR3357.

Example:

pnpm add @khanacademy/perseus@PR3357

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3357

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3357

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

olc-5.0.c6fc7 schema-change Attached to PRs when we detect Perseus Schema changes in it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant