Skip to content

[Experiment] AbsoluteValue with Claude implementation#3304

Draft
handeyeco wants to merge 6 commits intomainfrom
LEMS-3936/absolute-ai
Draft

[Experiment] AbsoluteValue with Claude implementation#3304
handeyeco wants to merge 6 commits intomainfrom
LEMS-3936/absolute-ai

Conversation

@handeyeco
Copy link
Contributor

Summary:

Issue: LEMS-3936

Test plan:

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

github-actions bot commented Mar 4, 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-12 16:03:28.525229164 +0000
+++ /home/runner/work/_temp/branch-compare/pr/schema.d.ts	2026-03-12 16:03:17.209092104 +0000
@@ -869,6 +869,7 @@
     size: "small" | "medium" | "large";
   };
   export type PerseusGraphType =
+    | PerseusGraphTypeAbsoluteValue
     | PerseusGraphTypeAngle
     | PerseusGraphTypeCircle
     | PerseusGraphTypeLinear
@@ -945,6 +946,11 @@
     coords?: Coord[] | null;
     startCoords?: Coord[];
   };
+  export type PerseusGraphTypeAbsoluteValue = {
+    type: "absolute_value";
+    coords?: [Coord, Coord] | null;
+    startCoords?: [Coord, Coord];
+  };
   export type PerseusGraphTypeRay = {
     type: "ray";
     coords?: CollinearTuple | null;
@@ -993,11 +999,16 @@
     type: "sinusoid";
     coords: CollinearTuple;
   };
+  type AbsoluteValueGraphCorrect = {
+    type: "absolute_value";
+    coords: [Coord, Coord];
+  };
   type RayGraphCorrect = {
     type: "ray";
     coords: CollinearTuple;
   };
   export type PerseusGraphCorrectType =
+    | AbsoluteValueGraphCorrect
     | AngleGraphCorrect
     | CircleGraphCorrect
     | LinearGraphCorrect

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

🛠️ Item Splitting: 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/index.item-splitting.js /home/runner/work/_temp/branch-compare/pr/index.item-splitting.js
--- /home/runner/work/_temp/branch-compare/base/index.item-splitting.js	2026-03-12 16:03:53.122351113 +0000
+++ /home/runner/work/_temp/branch-compare/pr/index.item-splitting.js	2026-03-12 16:03:28.080743319 +0000
@@ -100,7 +100,7 @@
 
 const lockedFigureColorNames=["blue","green","grayH","purple","pink","orange","red"];const plotterPlotTypes=["bar","line","pic","histogram","dotplot"];
 
-const pairOfNumbers=pair(number,number);const parsePerseusGraphTypeAngle=object({type:constant("angle"),showAngles:optional(boolean),allowReflexAngles:optional(boolean),angleOffsetDeg:optional(number),snapDegrees:optional(number),match:optional(constant("congruent")),coords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers)),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeCircle=object({type:constant("circle"),center:optional(pairOfNumbers),radius:optional(number),startCoords:optional(object({center:pairOfNumbers,radius:number}))});const parsePerseusGraphTypeLinear=object({type:constant("linear"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeLinearSystem=object({type:constant("linear-system"),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers)))});const parsePerseusGraphTypeNone=object({type:constant("none")});const parsePerseusGraphTypePoint=object({type:constant("point"),numPoints:optional(union(number).or(constant("unlimited")).parser),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypePolygon=object({type:constant("polygon"),numSides:optional(union(number).or(constant("unlimited")).parser),showAngles:optional(boolean),showSides:optional(boolean),snapTo:optional(enumeration("grid","angles","sides")),match:optional(enumeration("similar","congruent","approx","exact")),startCoords:optional(array(pairOfNumbers)),coords:optional(nullable(array(pairOfNumbers)))});const parsePerseusGraphTypeQuadratic=object({type:constant("quadratic"),coords:optional(nullable(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeRay=object({type:constant("ray"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeSegment=object({type:constant("segment"),numSegments:optional(number),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers)))});const parsePerseusGraphTypeSinusoid=object({type:constant("sinusoid"),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers))});const parsePerseusGraphType=discriminatedUnionOn("type").withBranch("angle",parsePerseusGraphTypeAngle).withBranch("circle",parsePerseusGraphTypeCircle).withBranch("linear",parsePerseusGraphTypeLinear).withBranch("linear-system",parsePerseusGraphTypeLinearSystem).withBranch("none",parsePerseusGraphTypeNone).withBranch("point",parsePerseusGraphTypePoint).withBranch("polygon",parsePerseusGraphTypePolygon).withBranch("quadratic",parsePerseusGraphTypeQuadratic).withBranch("ray",parsePerseusGraphTypeRay).withBranch("segment",parsePerseusGraphTypeSegment).withBranch("sinusoid",parsePerseusGraphTypeSinusoid).parser;const parseLockedFigureColor=enumeration(...lockedFigureColorNames);const parseLockedFigureFillType=enumeration("none","white","translucent","solid");const parseLockedLineStyle=enumeration("solid","dashed");const parseStrokeWeight=defaulted(enumeration("medium","thin","thick"),()=>"medium");const parseLockedLabelType=object({type:constant("label"),coord:pairOfNumbers,text:string,color:parseLockedFigureColor,size:enumeration("small","medium","large")});const parseLockedPointType=object({type:constant("point"),coord:pairOfNumbers,color:parseLockedFigureColor,filled:boolean,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedLineType=object({type:constant("line"),kind:enumeration("line","ray","segment"),points:pair(parseLockedPointType,parseLockedPointType),color:parseLockedFigureColor,lineStyle:parseLockedLineStyle,showPoint1:defaulted(boolean,()=>false),showPoint2:defaulted(boolean,()=>false),weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedVectorType=object({type:constant("vector"),points:pair(pairOfNumbers,pairOfNumbers),color:parseLockedFigureColor,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedEllipseType=object({type:constant("ellipse"),center:pairOfNumbers,radius:pairOfNumbers,angle:number,color:parseLockedFigureColor,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedPolygonType=object({type:constant("polygon"),points:array(pairOfNumbers),color:parseLockedFigureColor,showVertices:boolean,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedFunctionDomain=defaulted(pair(defaulted(number,()=>-Infinity),defaulted(number,()=>Infinity)),()=>[-Infinity,Infinity]);const parseLockedFunctionType=object({type:constant("function"),color:parseLockedFigureColor,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,equation:string,directionalAxis:enumeration("x","y"),domain:parseLockedFunctionDomain,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedFigure=discriminatedUnionOn("type").withBranch("point",parseLockedPointType).withBranch("line",parseLockedLineType).withBranch("vector",parseLockedVectorType).withBranch("ellipse",parseLockedEllipseType).withBranch("polygon",parseLockedPolygonType).withBranch("function",parseLockedFunctionType).withBranch("label",parseLockedLabelType).parser;const parseLabelLocation=union(enumeration("onAxis","alongEdge")).or(pipeParsers(constant("")).then(convert(()=>"onAxis")).parser).parser;const parseInteractiveGraphWidget=parseWidget(constant("interactive-graph"),object({step:pairOfNumbers,gridStep:optional(pairOfNumbers),snapStep:optional(pairOfNumbers),backgroundImage:optional(parsePerseusImageBackground),markings:enumeration("graph","grid","none","axes"),labels:optional(array(string)),labelLocation:optional(parseLabelLocation),showProtractor:boolean,showRuler:optional(boolean),showTooltips:optional(boolean),rulerLabel:optional(string),rulerTicks:optional(number),range:pair(pairOfNumbers,pairOfNumbers),showAxisArrows:defaulted(object({xMin:boolean,xMax:boolean,yMin:boolean,yMax:boolean}),()=>({xMin:true,xMax:true,yMin:true,yMax:true})),graph:defaulted(parsePerseusGraphType,()=>({type:"linear"})),correct:defaulted(parsePerseusGraphType,()=>({type:"linear"})),lockedFigures:defaulted(array(parseLockedFigure),()=>[]),fullGraphAriaLabel:optional(string),fullGraphAriaDescription:optional(string)}));
+const pairOfNumbers=pair(number,number);const parsePerseusGraphTypeAngle=object({type:constant("angle"),showAngles:optional(boolean),allowReflexAngles:optional(boolean),angleOffsetDeg:optional(number),snapDegrees:optional(number),match:optional(constant("congruent")),coords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers)),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeCircle=object({type:constant("circle"),center:optional(pairOfNumbers),radius:optional(number),startCoords:optional(object({center:pairOfNumbers,radius:number}))});const parsePerseusGraphTypeLinear=object({type:constant("linear"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeLinearSystem=object({type:constant("linear-system"),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers)))});const parsePerseusGraphTypeNone=object({type:constant("none")});const parsePerseusGraphTypePoint=object({type:constant("point"),numPoints:optional(union(number).or(constant("unlimited")).parser),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypePolygon=object({type:constant("polygon"),numSides:optional(union(number).or(constant("unlimited")).parser),showAngles:optional(boolean),showSides:optional(boolean),snapTo:optional(enumeration("grid","angles","sides")),match:optional(enumeration("similar","congruent","approx","exact")),startCoords:optional(array(pairOfNumbers)),coords:optional(nullable(array(pairOfNumbers)))});const parsePerseusGraphTypeQuadratic=object({type:constant("quadratic"),coords:optional(nullable(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeRay=object({type:constant("ray"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeSegment=object({type:constant("segment"),numSegments:optional(number),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers)))});const parsePerseusGraphTypeSinusoid=object({type:constant("sinusoid"),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers))});const parsePerseusGraphTypeAbsoluteValue=object({type:constant("absolute_value"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers))});const parsePerseusGraphType=discriminatedUnionOn("type").withBranch("absolute_value",parsePerseusGraphTypeAbsoluteValue).withBranch("angle",parsePerseusGraphTypeAngle).withBranch("circle",parsePerseusGraphTypeCircle).withBranch("linear",parsePerseusGraphTypeLinear).withBranch("linear-system",parsePerseusGraphTypeLinearSystem).withBranch("none",parsePerseusGraphTypeNone).withBranch("point",parsePerseusGraphTypePoint).withBranch("polygon",parsePerseusGraphTypePolygon).withBranch("quadratic",parsePerseusGraphTypeQuadratic).withBranch("ray",parsePerseusGraphTypeRay).withBranch("segment",parsePerseusGraphTypeSegment).withBranch("sinusoid",parsePerseusGraphTypeSinusoid).parser;const parseLockedFigureColor=enumeration(...lockedFigureColorNames);const parseLockedFigureFillType=enumeration("none","white","translucent","solid");const parseLockedLineStyle=enumeration("solid","dashed");const parseStrokeWeight=defaulted(enumeration("medium","thin","thick"),()=>"medium");const parseLockedLabelType=object({type:constant("label"),coord:pairOfNumbers,text:string,color:parseLockedFigureColor,size:enumeration("small","medium","large")});const parseLockedPointType=object({type:constant("point"),coord:pairOfNumbers,color:parseLockedFigureColor,filled:boolean,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedLineType=object({type:constant("line"),kind:enumeration("line","ray","segment"),points:pair(parseLockedPointType,parseLockedPointType),color:parseLockedFigureColor,lineStyle:parseLockedLineStyle,showPoint1:defaulted(boolean,()=>false),showPoint2:defaulted(boolean,()=>false),weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedVectorType=object({type:constant("vector"),points:pair(pairOfNumbers,pairOfNumbers),color:parseLockedFigureColor,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedEllipseType=object({type:constant("ellipse"),center:pairOfNumbers,radius:pairOfNumbers,angle:number,color:parseLockedFigureColor,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedPolygonType=object({type:constant("polygon"),points:array(pairOfNumbers),color:parseLockedFigureColor,showVertices:boolean,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedFunctionDomain=defaulted(pair(defaulted(number,()=>-Infinity),defaulted(number,()=>Infinity)),()=>[-Infinity,Infinity]);const parseLockedFunctionType=object({type:constant("function"),color:parseLockedFigureColor,strokeStyle:parseLockedLineStyle,weight:parseStrokeWeight,equation:string,directionalAxis:enumeration("x","y"),domain:parseLockedFunctionDomain,labels:defaulted(array(parseLockedLabelType),()=>[]),ariaLabel:optional(string)});const parseLockedFigure=discriminatedUnionOn("type").withBranch("point",parseLockedPointType).withBranch("line",parseLockedLineType).withBranch("vector",parseLockedVectorType).withBranch("ellipse",parseLockedEllipseType).withBranch("polygon",parseLockedPolygonType).withBranch("function",parseLockedFunctionType).withBranch("label",parseLockedLabelType).parser;const parseLabelLocation=union(enumeration("onAxis","alongEdge")).or(pipeParsers(constant("")).then(convert(()=>"onAxis")).parser).parser;const parseInteractiveGraphWidget=parseWidget(constant("interactive-graph"),object({step:pairOfNumbers,gridStep:optional(pairOfNumbers),snapStep:optional(pairOfNumbers),backgroundImage:optional(parsePerseusImageBackground),markings:enumeration("graph","grid","none","axes"),labels:optional(array(string)),labelLocation:optional(parseLabelLocation),showProtractor:boolean,showRuler:optional(boolean),showTooltips:optional(boolean),rulerLabel:optional(string),rulerTicks:optional(number),range:pair(pairOfNumbers,pairOfNumbers),showAxisArrows:defaulted(object({xMin:boolean,xMax:boolean,yMin:boolean,yMax:boolean}),()=>({xMin:true,xMax:true,yMin:true,yMax:true})),graph:defaulted(parsePerseusGraphType,()=>({type:"linear"})),correct:defaulted(parsePerseusGraphType,()=>({type:"linear"})),lockedFigures:defaulted(array(parseLockedFigure),()=>[]),fullGraphAriaLabel:optional(string),fullGraphAriaDescription:optional(string)}));
 
 const parseLabelImageWidget=parseWidget(constant("label-image"),object({choices:array(string),imageUrl:string,imageAlt:string,imageHeight:number,imageWidth:number,markers:array(object({answers:defaulted(array(string),()=>[]),label:string,x:number,y:number})),hideChoicesFromInstructions:boolean,multipleAnswers:boolean,static:defaulted(boolean,()=>false)}));
 

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Size Change: +1.31 kB (+0.27%)

Total Size: 487 kB

Filename Size Change
packages/perseus-core/dist/es/index.item-splitting.js 11.9 kB +17 B (+0.14%)
packages/perseus-core/dist/es/index.js 24.9 kB +49 B (+0.2%)
packages/perseus-editor/dist/es/index.js 100 kB +225 B (+0.22%)
packages/perseus-score/dist/es/index.js 9.37 kB +111 B (+1.2%)
packages/perseus/dist/es/index.js 188 kB +716 B (+0.38%)
packages/perseus/dist/es/strings.js 7.67 kB +197 B (+2.64%)
ℹ️ 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-linter/dist/es/index.js 8.82 kB
packages/perseus-utils/dist/es/index.js 403 B
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 4, 2026

npm Snapshot: Published

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

Example:

pnpm add @khanacademy/perseus@PR3304

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

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

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

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

): PerseusGraphTypeAbsoluteValue {
return {
type: "absolute_value",
coords: [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: double check this, it doesn't seem to match the others.


type AbsoluteValueGraphCorrect = {
type: "absolute_value";
coords: [Coord, Coord];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: the other types use CollinearTuple.

const {getSinusoidCoefficients, getQuadraticCoefficients} = coefficients;

// Returns {m, h, k} for f(x) = m * |x - h| + k, or undefined if coords share the same x.
function getAbsoluteValueCoefficients(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: it defined this twice

const correctCoeffs = getAbsoluteValueCoefficients(
rubric.correct.coords,
);
if (approximateDeepEqual(guessCoeffs, correctCoeffs)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be approximate? Seems like it should be exact?

const {getClockwiseAngle} = angles;
const {getSinusoidCoefficients, getQuadraticCoefficients} = coefficients;

// Returns {m, h, k} for f(x) = m * |x - h| + k, or undefined if coords share the same x.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: needs a better, more human readable explanation.

@handeyeco handeyeco changed the title test Claude on ticket [Experiment] AbsoluteValue with Claude implementation Mar 12, 2026
handeyeco added a commit that referenced this pull request Mar 13, 2026
## Summary:

- Originally I was playing with Claude and got it to implement the chart: #3304
- Later we added a guide to adding new graph types: #3320
- This ticket is ignoring Perseus#3304 and uses the guide in Perseus#3320 to create a planning document for AbsoluteValue

Issue: LEMS-3936

Author: handeyeco

Reviewers: claude[bot], ivyolamit, SonicScrewdriver

Required Reviewers:

Approved By: ivyolamit

Checks: ⚪️ 1 check is neutral, ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3334
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

item-splitting-change olc-5.0.97b91 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