feat: improve worksheet
This commit is contained in:
@@ -571,6 +571,77 @@ fn asset_schema() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// The worked solution, and for an open-response item how it is graded.
|
||||
fn solution_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "The answer, the reasoning, and the rubric. Rendered in the solutions \
|
||||
document and the answer key, never in a question paper.",
|
||||
"properties": {
|
||||
"model_answer": {
|
||||
"type": "string",
|
||||
"description": "For an open-response item, the response a full-credit student \
|
||||
writes; for a choice item, an optional one-line statement of the key."
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "The worked reasoning a student learns from. The body of the \
|
||||
solutions entry."
|
||||
},
|
||||
"rubric": { "type": "array", "items": rubric_criterion_schema() },
|
||||
"accepted": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Responses a short constructed answer is accepted as."
|
||||
},
|
||||
"review": {
|
||||
"type": "array",
|
||||
"items": citation_schema(),
|
||||
"description": "Where to look again after missing this item."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// One rubric line for an open-response item.
|
||||
fn rubric_criterion_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"required": ["description"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": text("What earns the points on this line."),
|
||||
"points": { "type": "number", "minimum": 0.0 }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A citation into the reference registry, written as an object or a bare string.
|
||||
fn citation_schema() -> Value {
|
||||
json!({
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "A citation, unparsed." },
|
||||
citation_mapping_schema()
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/// The object form of a citation.
|
||||
fn citation_mapping_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": text("Citation key into `references`."),
|
||||
"locator": { "type": "string", "description": "Where inside the work: §6.1, pp. 4-9." },
|
||||
"path": { "type": "string", "description": "Joined to the reference base_url." },
|
||||
"url": { "type": "string" },
|
||||
"text": { "type": "string" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The schema for authored design intent.
|
||||
fn design_schema() -> Value {
|
||||
json!({
|
||||
@@ -738,9 +809,10 @@ fn item_identity_properties() -> Value {
|
||||
"cognitive_process": cognitive_process(),
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["single_best_answer", "multiple_response", "true_false"],
|
||||
"description": "single_best_answer requires exactly one keyed option; \
|
||||
multiple_response requires at least two."
|
||||
"enum": ["single_best_answer", "multiple_response", "true_false", "open_response"],
|
||||
"description": "single_best_answer keys exactly one option; multiple_response keys \
|
||||
two or more; open_response takes no options and is graded from its \
|
||||
solution."
|
||||
},
|
||||
"bonus": { "type": "boolean" },
|
||||
"points": { "type": "number", "exclusiveMinimum": 0.0 },
|
||||
@@ -767,8 +839,10 @@ fn item_content_properties() -> Value {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 8,
|
||||
"description": "Absent for an open_response item; at least two for any choice format.",
|
||||
"items": option_schema()
|
||||
},
|
||||
"solution": solution_schema(),
|
||||
"learning_objectives": string_array(
|
||||
"Objective ids this item measures. Reports aggregate on these, so an item with none \
|
||||
contributes to nothing."
|
||||
@@ -807,7 +881,7 @@ fn item_schema() -> Value {
|
||||
}
|
||||
json!({
|
||||
"type": "object",
|
||||
"required": ["id", "level", "stem", "options"],
|
||||
"required": ["id", "level", "stem"],
|
||||
"additionalProperties": false,
|
||||
"properties": Value::Object(properties)
|
||||
})
|
||||
|
||||
+10
-4
@@ -421,7 +421,7 @@ pub fn lint_item(entry: &Entry, course: &CourseFile, t: &Thresholds) -> Vec<Find
|
||||
});
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- clarity
|
||||
// --- clarity
|
||||
let stem = it.stem.trim();
|
||||
let stem_lower = stem.to_lowercase();
|
||||
let words: Vec<&str> = stem.split_whitespace().collect();
|
||||
@@ -513,7 +513,7 @@ pub fn lint_item(entry: &Entry, course: &CourseFile, t: &Thresholds) -> Vec<Find
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- cueing
|
||||
// --- cueing
|
||||
let keys: Vec<&crate::item::Choice> = it.options.iter().filter(|o| o.correct).collect();
|
||||
let distractors: Vec<&crate::item::Choice> = it.options.iter().filter(|o| !o.correct).collect();
|
||||
|
||||
@@ -659,7 +659,7 @@ pub fn lint_item(entry: &Entry, course: &CourseFile, t: &Thresholds) -> Vec<Find
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- completeness
|
||||
// --- completeness
|
||||
// These are only worth insisting on once an item is meant to be used.
|
||||
let is_ready = matches!(it.status, Status::Approved | Status::InReview);
|
||||
if is_ready {
|
||||
@@ -749,7 +749,7 @@ pub fn lint_item(entry: &Entry, course: &CourseFile, t: &Thresholds) -> Vec<Find
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- evidence
|
||||
// --- evidence
|
||||
if !it.calibration_is_current() {
|
||||
push(
|
||||
Rule::StaleCalibration,
|
||||
@@ -868,6 +868,12 @@ fn lint_option_counts(catalog: &Catalog) -> Vec<Finding> {
|
||||
if e.item.status == Status::Retired {
|
||||
continue;
|
||||
}
|
||||
// An open-response item carries no options, so it is neither part of the
|
||||
// count norm nor able to deviate from it. Leaving it out keeps a bank of
|
||||
// four-option questions from reporting every essay as an odd count.
|
||||
if !e.item.has_options() {
|
||||
continue;
|
||||
}
|
||||
by_bank.entry(e.bank.as_str()).or_default().push(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn select(
|
||||
let seed = blueprint.seed.unwrap_or(0);
|
||||
let mut notes = Vec::new();
|
||||
|
||||
// ------------------------------------------------------------------ pool
|
||||
// --- pool
|
||||
let eligible: Vec<&crate::catalog::Entry> = catalog
|
||||
.assemblable()
|
||||
.into_iter()
|
||||
@@ -101,7 +101,7 @@ pub fn select(
|
||||
let mut chosen: Vec<String> = Vec::new();
|
||||
let mut per_bank: BTreeMap<String, usize> = BTreeMap::new();
|
||||
|
||||
// ---------------------------------------------------- objective minimums
|
||||
// --- objective minimums
|
||||
// Placed first, because a coverage requirement is the constraint most likely
|
||||
// to become unsatisfiable once the level quotas are full.
|
||||
for (objective, needed) in &blueprint.objective_minimums {
|
||||
@@ -140,7 +140,7 @@ pub fn select(
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- level quotas
|
||||
// --- level quotas
|
||||
let mut scored: Vec<String> = Vec::new();
|
||||
for (level, want) in &blueprint.level_counts {
|
||||
if *want == 0 {
|
||||
@@ -175,7 +175,7 @@ pub fn select(
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ bonus items
|
||||
// --- bonus items
|
||||
let mut bonus: Vec<String> = Vec::new();
|
||||
for (level, want) in &blueprint.bonus_counts {
|
||||
if *want == 0 {
|
||||
|
||||
Reference in New Issue
Block a user