feat: splitting
This commit is contained in:
+22
-53
@@ -352,8 +352,10 @@ fn validate_item(
|
||||
if it.stem.trim().is_empty() {
|
||||
issues.push("empty stem".into());
|
||||
}
|
||||
if it.version == 0 {
|
||||
issues.push("version must be at least 1".into());
|
||||
if let Some(replaced) = &it.supersedes {
|
||||
if *replaced == it.id {
|
||||
issues.push("supersedes names this item".into());
|
||||
}
|
||||
}
|
||||
|
||||
// --- options ----
|
||||
@@ -379,15 +381,22 @@ fn validate_item(
|
||||
if o.text.trim().is_empty() {
|
||||
issues.push(format!("option {pos}: empty text"));
|
||||
}
|
||||
let letter_ok = o.id.len() == 1
|
||||
&& o.id
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_uppercase() && c <= 'H')
|
||||
.unwrap_or(false);
|
||||
if !letter_ok {
|
||||
// Two forms are accepted: the 2.0 name, and the letter that preceded
|
||||
// it. A bank migrates when `coursebank migrate options` is run on it,
|
||||
// not when the tool is upgraded, so a course mid-migration still loads.
|
||||
let slug_ok = o.id.strip_prefix("o-").is_some_and(|rest| {
|
||||
!rest.is_empty()
|
||||
&& !rest.starts_with('-')
|
||||
&& !rest.ends_with('-')
|
||||
&& !rest.contains("--")
|
||||
&& rest
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
});
|
||||
if !slug_ok && !Item::is_legacy_option_id(&o.id) {
|
||||
issues.push(format!(
|
||||
"option {pos}: id `{}` must be a single letter A through H",
|
||||
"option {pos}: id `{}` is neither a name such as `o-fourth-line` nor a pre-2.0 \
|
||||
letter A through H",
|
||||
o.id
|
||||
));
|
||||
}
|
||||
@@ -514,10 +523,10 @@ fn validate_item(
|
||||
));
|
||||
}
|
||||
}
|
||||
for letter in c.option_stats.keys() {
|
||||
if it.option(letter).is_none() {
|
||||
for option in c.option_stats.keys() {
|
||||
if it.option(option).is_none() {
|
||||
issues.push(format!(
|
||||
"calibration.option_stats has `{letter}`, which is not an option of this item"
|
||||
"calibration.option_stats has `{option}`, which is not an option of this item"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -533,25 +542,6 @@ fn validate_item(
|
||||
}
|
||||
}
|
||||
|
||||
// --- history must be coherent ------
|
||||
let mut last_version = 0u32;
|
||||
for (i, h) in it.history.iter().enumerate() {
|
||||
if h.version <= last_version {
|
||||
issues.push(format!(
|
||||
"history entry {} has version {} which does not increase",
|
||||
i + 1,
|
||||
h.version
|
||||
));
|
||||
}
|
||||
last_version = h.version;
|
||||
}
|
||||
if !it.history.is_empty() && last_version > it.version {
|
||||
issues.push(format!(
|
||||
"history records version {last_version} but the item says version {}",
|
||||
it.version
|
||||
));
|
||||
}
|
||||
|
||||
// --- retirement -----
|
||||
if it.retired.is_some() && it.status != Status::Retired {
|
||||
issues.push(format!(
|
||||
@@ -1027,27 +1017,6 @@ learning_targets:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_versions_must_increase() {
|
||||
let b = bank(
|
||||
r#"
|
||||
- id: q-a-001
|
||||
version: 2
|
||||
status: draft
|
||||
level: 1
|
||||
stem: s
|
||||
options:
|
||||
- { id: A, text: a, correct: true }
|
||||
- { id: B, text: b }
|
||||
history:
|
||||
- { version: 2, date: 2026-01-01, change: second }
|
||||
- { version: 1, date: 2026-01-02, change: first }
|
||||
"#,
|
||||
);
|
||||
let issues = b.validate(None);
|
||||
assert!(issues.iter().any(|i| i.contains("does not increase")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_counts_exclude_drafts_and_bonuses() {
|
||||
let b = bank(
|
||||
|
||||
Reference in New Issue
Block a user