Files
catena/crates/citefind/tests/references.rs
T
funkpower c071f84829 citefind: the references in a bibliography, and what could not be read off them
Cuts a reference list into entries — at the note numbers where there are any,
at the layout where there are not — and reads each as far as its punctuation
allows. Fields that would not parse are named in `unsettled` rather than
guessed at, and `--prompt` asks a model about exactly those and no others.

Over 202 real reference sections from the corpus: 8,566 references, 95% with
a title, 82% with a year, 46% settling every field with no model involved.
A title is refused where the entry was never divided, so no record reports
its own text back as the work it cites.

`ibid.` is flagged and left alone — it points at a work, and the entry does
not say which. A rule of dashes is filled in, because the style defines it as
the author above.

chunkmd gains `note_starts` and `cites_something`: recognising a page of
notes and splitting one are the same knowledge, and two copies of that
pattern would drift.
2026-09-20 10:38:48 -04:00

178 lines
5.7 KiB
Rust

//! Reading a block of references end to end.
use citefind::{Kind, read};
/// A page of endnotes as conversion leaves it: numbers, anchors, and a
/// discursive note in among the citations.
const NOTES: &str = "\
[1.](part0011.html#ke_00001_00003) John MacGowan, *The Imperial History of China* (London: Curzon, 1897), p. 154.
2. I am grateful to the seminar at Leiden for pressing me on this.
[3](text0007.xhtml#en3). Johanna Bockman, *Markets in the Name of Socialism* (Stanford, CA: Stanford University Press, 2011), 40-42.
4. Ibid., p. 44.
";
#[test]
fn a_page_of_notes_keeps_its_numbering_and_drops_its_asides() {
let found = read(NOTES);
assert_eq!(found.len(), 3, "{found:#?}");
assert_eq!(
found
.iter()
.map(|one| one.marker.as_deref())
.collect::<Vec<_>>(),
[Some("1"), Some("3"), Some("4")]
);
// Numbered from zero in the order they appeared, whatever the markers say.
assert_eq!(
found.iter().map(|one| one.idx).collect::<Vec<_>>(),
[0, 1, 2]
);
assert_eq!(found[0].authors, ["John MacGowan"]);
assert_eq!(found[0].container.as_deref(), Some("Curzon"));
assert_eq!(found[1].year, Some(2011));
assert_eq!(found[1].kind, Kind::Book);
assert!(found[2].continuation);
}
#[test]
fn a_rule_of_dashes_means_the_author_above() {
let block = "\
Smith, John. *The First Book*. Oxford: Clarendon Press, 1998.
———. *The Second Book*. Oxford: Clarendon Press, 2001.
——— *The Third Book*. Oxford: Clarendon Press, 2004.
Doe, Jane. *Something Else*. Cambridge: University Press, 2005.
———. *And Another*. Cambridge: University Press, 2007.";
let found = read(block);
assert_eq!(found.len(), 5, "{found:#?}");
assert_eq!(
found
.iter()
.map(|one| one.authors.clone())
.collect::<Vec<_>>(),
[
vec!["Smith, John"],
vec!["Smith, John"],
vec!["Smith, John"],
vec!["Doe, Jane"],
vec!["Doe, Jane"],
]
);
assert_eq!(found[2].title.as_deref(), Some("The Third Book"));
}
#[test]
fn an_ibid_does_not_inherit_an_author_the_way_a_dash_does() {
let block = "\
Smith, John. *The First Book*. Oxford: Clarendon Press, 1998.
Ibid., p. 44.";
let found = read(block);
assert_eq!(found.len(), 2);
assert!(found[1].authors.is_empty(), "{:#?}", found[1]);
assert!(found[1].continuation);
}
#[test]
fn every_record_says_what_it_could_not_settle() {
let found = read(
"Marron, Kevin Ritual Abuse, Seal Books, 1988\n\nAnother, Line. Of nothing much, 1990.",
);
for one in &found {
assert_eq!(
one.unsettled,
one.missing()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
);
}
}
#[test]
fn the_same_block_reads_the_same_way_every_time() {
assert_eq!(read(NOTES), read(NOTES));
}
/// A block mixing one entry that reads cleanly, one that gives up nothing
/// but a title, and a continuation.
const MIXED: &str = "\
1. John MacGowan, *The Imperial History of China* (London: Curzon, 1897), p. 154.
2. Marron, Kevin Ritual Abuse, Seal Books, 1988.
3. Ibid., p. 44.
";
#[test]
fn a_prompt_asks_about_the_entries_that_did_not_settle_and_no_others() {
let found = read(MIXED);
assert_eq!(found.len(), 3, "{found:#?}");
let prompt = citefind::prompt::compose(&found).expect("something unsettled");
// The entry that read cleanly is not in the question, and neither is the
// continuation — it has nothing of its own to give.
assert!(!prompt.contains("MacGowan"), "{prompt}");
assert!(!prompt.contains("Ibid"), "{prompt}");
assert!(prompt.contains("Ritual Abuse"), "{prompt}");
assert!(prompt.contains("wanted:"), "{prompt}");
}
#[test]
fn a_prompt_is_only_composed_where_there_is_something_to_ask() {
let settled = read(
"1. John MacGowan, *The Imperial History of China* (London: Curzon, 1897), p. 154.\n2. Johanna Bockman, *Markets in the Name of Socialism* (Stanford, CA: Stanford University Press, 2011), 40.",
);
assert!(
settled.iter().all(|one| one.unsettled.is_empty()),
"{settled:#?}"
);
assert_eq!(citefind::prompt::compose(&settled), None);
}
#[test]
fn a_reply_about_an_entry_that_settled_does_not_overrule_the_page() {
let mut found = read(MIXED);
let before = found[0].clone();
let filled = citefind::prompt::merge(
&mut found,
r#"{"0": {"title": "Invented", "container": "Invented"}, "2": {"title": "Invented"}}"#,
)
.expect("a reply");
assert_eq!(filled, 0);
assert_eq!(found[0], before);
assert_eq!(found[2].title, None);
}
#[test]
fn a_reply_fills_in_only_what_was_asked_about() {
let mut found = read("Marron, Kevin Ritual Abuse, Seal Books, 1988");
assert_eq!(found.len(), 1);
assert!(found[0].unsettled.contains(&"authors".to_string()));
let filled = citefind::prompt::merge(
&mut found,
"Sure!\n```json\n{\"0\": {\"authors\": [\"Marron, Kevin\"], \"year\": 1066}}\n```",
)
.expect("a reply");
assert_eq!(filled, 1);
assert_eq!(found[0].authors, ["Marron, Kevin"]);
// The year was read off the page, so the model's does not replace it.
assert_eq!(found[0].year, Some(1988));
assert!(!found[0].unsettled.contains(&"authors".to_string()));
}
#[test]
fn a_reply_that_is_not_json_is_refused_rather_than_half_applied() {
let mut found = read(NOTES);
let before = found.clone();
assert!(citefind::prompt::merge(&mut found, "I could not tell.").is_err());
assert_eq!(found, before);
}
#[test]
fn nothing_citation_shaped_gives_nothing() {
assert!(read("Bibliography\n\nSee below.\n\n233\n").is_empty());
}