Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSS: Improve types for validation #2002

Open
wants to merge 10 commits into
base: feature/client-validation
Choose a base branch
from

Conversation

jeremywiebe
Copy link
Collaborator

@jeremywiebe jeremywiebe commented Dec 13, 2024

Summary:

This PR begins working out the types for Scoring, Validation, and how they relate to our full widget options types. There are currently three "trees" of types that are shaped as a map of widgetId to something. They are:

  • Full widget options (starting from PerseusRenderer)
  • Scoring data - used to score the learner's guess (user input)
  • Validation data - a shared subset (of Render and Scoring data) used to do empty widget checking (aka validation). This helps the frontend to know if the question is scorable yet.

Finally, there is also a widget map known as User Input. This map is a map of widget ids from the item to the user input the learner has entered so far.

Issue: LEMS-2561

Test plan:

yarn typecheck (especially, the new validation.typetest.ts file!)
yarn test (just to be sure)

@jeremywiebe jeremywiebe self-assigned this Dec 13, 2024
Copy link
Contributor

github-actions bot commented Dec 13, 2024

Size Change: +115 B (+0.01%)

Total Size: 1.27 MB

Filename Size Change
packages/perseus/dist/es/index.js 416 kB +115 B (+0.03%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 4.27 kB
packages/math-input/dist/es/index.js 77.9 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-core/dist/es/index.js 1.48 kB
packages/perseus-editor/dist/es/index.js 688 kB
packages/perseus-linter/dist/es/index.js 22.2 kB
packages/perseus/dist/es/strings.js 4.12 kB
packages/pure-markdown/dist/es/index.js 3.66 kB
packages/simple-markdown/dist/es/index.js 12.5 kB

compressed-size-action

@jeremywiebe jeremywiebe force-pushed the jer/client-validation-3 branch from 91dd867 to c619dd8 Compare December 13, 2024 23:24
@jeremywiebe jeremywiebe changed the base branch from jer/client-validation-3 to feature/client-validation December 14, 2024 01:40
@jeremywiebe jeremywiebe force-pushed the jer/client-validation-4 branch from d84a790 to 8629c57 Compare December 16, 2024 19:17
| PerseusSorterRubric
| PerseusTableRubric;

export type UserInput =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is moved down below all of the Rubric/Scoring Data types together with the various widget UserInput types. #organizing

export interface RubricRegistry {
categorizer: PerseusCategorizerScoringData;
"cs-program": PerseusCSProgramRubric;
// definition: PerseusDefinitionScoringData;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I really am not a fan of how this works right now. The fact that this is not a full listing of all widgets because we don't score all widgets worries me, but I don't think it makes sense to add a type for all widgets just to make this "whole".

I'm tempted to leave it like this for now and as we work further hopefully something resolves to a better solution.

Also open to ideas.

Copy link
Contributor

Choose a reason for hiding this comment

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

If not all widgets have ScoringData, why don't we just remove the inner comments and add a comment to the whole interface (just above line 241) saying that? We could keep a list of which widgets currently don't have scoring data in that comment so it doesn't muddy the interface so to speak. Or is there a reason why we should have an entry for every widget in this interface?

Copy link
Contributor

Choose a reason for hiding this comment

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

My vote is remove entries for widgets we don't score.

Copy link
Contributor

github-actions bot commented Dec 16, 2024

npm Snapshot: Published

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

Example:

yarn add @khanacademy/perseus@PR2002

If you are working in Khan Academy's webapp, you can run:

./dev/tools/bump_perseus_version.sh -t PR2002

@jeremywiebe jeremywiebe marked this pull request as ready for review December 16, 2024 20:03
@jeremywiebe jeremywiebe requested review from handeyeco, Myranae and a team December 16, 2024 20:04
Comment on lines 15 to 16
// We can use a RubricMap as a ValidationDataMap
0 as any as RubricMap satisfies ValidationDataMap;
Copy link
Contributor

Choose a reason for hiding this comment

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

May want to update these to ScoringMap since we're removing the rubric terminology.

Comment on lines +40 to +43
// A utility type that constructs a widget map from a "registry interface".
// The keys of the registry should be the widget type (aka, "categorizer" or
// "radio", etc) and the value should be the option type stored in the value
// of the map.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you provide an example of the output from this function? Or an example usage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. So I've started developing the idea of a type registry (because we have full widget options types, scoring data types, frontend rendering types). Each of these is map of widget type to the option type for that widget.

If you look at the usage in this PR, we use the PerseusWidgetTypes interface to create the PerseusWidgetsMap type.

PerseusWidgetTypes looks like this (shortened a bit here):

export interface PerseusWidgetTypes {
    categorizer: CategorizerWidget;
    "cs-program": CSProgramWidget;
    definition: DefinitionWidget;
    ...
}

We use the helper:

export type PerseusWidgetsMap = MakeWidgetMap<PerseusWidgetTypes>;

And out comes the following type:

type PerseusWidgetsMap = {
    [x: `categorizer ${number}`]: CategorizerWidget;
    [x: `cs-program ${number}`]: CSProgramWidget;
    [x: `definition ${number}`]: DefinitionWidget;
    ...
}

So you can think of this helper as a sort of type "generator". It takes a registry of widget types and generates a map type where the ID of the map is a widget id and the value is the correct option type.

| PerseusTableUserInput;

export type UserInputMap = {[widgetId: string]: UserInput};
export interface RubricRegistry {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be ScoringDataRegistry? Doesn't have the same ring, but we're trying to move away from the rubric vocab, right? Also, should the Rubric types below be changed to ScoringData types or will we just handle that after landing my renaming PR when we get back? Might make sense to plan the order in which we land PRs, though it might make sense just to end the week with a fully merged feature branch with all our changes combined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I plan to land this PR after your scoring name changes... so I'll be sure to update the instances of Rubric in this PR before landing.

import type {RubricMap, ValidationDataMap} from "../validation.types";

// We can use a 'widgets' map from a PerseusRenderer as a ValidationDataMap
0 as any as PerseusRenderer["widgets"] satisfies ValidationDataMap;
Copy link
Contributor

Choose a reason for hiding this comment

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

This 0 as any as business really confused me when I first saw it; even now that I now what it's for, I still don't understand what it actually works. I honestly don't feel super comfortable having it in the code because it's both unclear and feels hacky. That being said, I'm fine if we keep it but I'd like it if we could link to some documentation on this pattern (or if it doesn't exist we can write our own).

Copy link
Contributor

Choose a reason for hiding this comment

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

++

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a pattern I saw in Ben's parsing code changes.

I've tried a different approach that will hopefully be clearer. My concern is that the two types being tested here (PerseusRenderer["widgets"] and ScoringDataMap) must remain compatible with ValidatioDataMap.

I don't another way to do that.

export interface RubricRegistry {
categorizer: PerseusCategorizerScoringData;
"cs-program": PerseusCSProgramRubric;
// definition: PerseusDefinitionScoringData;
Copy link
Contributor

Choose a reason for hiding this comment

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

My vote is remove entries for widgets we don't score.

* share functionality that understands how to traverse maps of `widget id` to
* `options`.
*/
export type RubricMap = {
Copy link
Contributor

Choose a reason for hiding this comment

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

So overall the point of this PR seems to be aimed at more closely matching widget-specific data to the widget ID?

Doesn't this reinforce the thing you and Nicole were against - tightly binding the widget type and the widget id? Like if we were to decide tomorrow that we were going to give all widgets UUIDs (which we should as we've seen in the GradedGroup issue), wouldn't that break this code?

I'm having trouble aligning the description of the PR and the ticket with the actual changes. This is like pre/post work for LEMS-2561?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, this PR drifted from what I initially typed out as the PR description. I'll update the description. It's basically type work to make the types we already use (full widget options, Scoring Data, Validation Data) a bit more clear and well-typed.

I agree that I don't like us buying further into the mapping of widget ID to data in our various maps. Maybe we can chat about that in a future PR? I'm also happy to have pushback and do more work in this PR to isolate the Renderer and outer code knowing so much about widget option details.

Comment on lines 289 to 297
export type RubricMap = {
[Property in keyof RubricRegistry as `${Property} ${number}`]: {
type: Property;
static?: boolean;
options: RubricRegistry[Property];
};
};

export type Rubric = RubricRegistry[keyof RubricRegistry];
Copy link
Contributor

Choose a reason for hiding this comment

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

Also pointing out the use of Rubric instead of ScoringData here as well.

Comment on lines 324 to 312
// A union type of all the widget user input types
export type UserInput = UserInputRegisry[keyof UserInputRegisry];

/**
* A map of widget IDs to user input types (strongly typed based on the format
* of the widget ID).
*/
export type UserInputMap = MakeWidgetMap<UserInputRegisry>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you only use the asterisk comments in certain situations? Just wondering when it should be the // version or the /** version.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, I should be consistent. I use /** */ for comments that would be useful to see if I used the type somewhere else.

Eg.
image

@jeremywiebe jeremywiebe force-pushed the jer/client-validation-4 branch from 930bb96 to 7ce25ce Compare December 19, 2024 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants