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.
This commit is contained in:
funkpower
2026-09-20 10:38:48 -04:00
parent 30ff3361ee
commit c071f84829
22 changed files with 2612 additions and 6 deletions
+76
View File
@@ -417,6 +417,82 @@ pub unsafe extern "C" fn catena_identify(
give_json(&crate::identify(name, bytes))
}
/// Read the references out of a bibliography or a page of notes. Returns a
/// JSON array, one object per reference.
///
/// # Safety
///
/// `text` must be NUL-terminated UTF-8.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn catena_find_citations(text: *const c_char) -> *mut c_char {
clear_error();
let Some(text) = (unsafe { borrow(text, "text") }) else {
return ptr::null_mut();
};
give_json(&crate::find_citations(text))
}
/// The question to put to a model about the references that did not parse.
/// Takes the JSON array `catena_find_citations` returned. Returns the prompt
/// as a JSON string, or NULL when every reference settled — an answer, not an
/// error: `catena_last_error` is NULL in that case.
///
/// # Safety
///
/// `references` must be NUL-terminated UTF-8 holding a JSON array.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn catena_citation_prompt(references: *const c_char) -> *mut c_char {
clear_error();
let Some(references) = (unsafe { borrow(references, "references") }) else {
return ptr::null_mut();
};
let references: Vec<citefind::Reference> = match serde_json::from_str(references) {
Ok(references) => references,
Err(why) => {
set_error(why.to_string());
return ptr::null_mut();
}
};
match crate::citation_prompt(&references) {
Some(prompt) => give_json(&prompt),
None => ptr::null_mut(),
}
}
/// Fill in what did not parse from a model's reply. Returns the JSON array
/// with the answer applied, or NULL and an error when the reply is not one
/// JSON object.
///
/// # Safety
///
/// `references` and `reply` must be NUL-terminated UTF-8.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn catena_merge_citation_reply(
references: *const c_char,
reply: *const c_char,
) -> *mut c_char {
clear_error();
let (Some(references), Some(reply)) = (unsafe { borrow(references, "references") }, unsafe {
borrow(reply, "reply")
}) else {
return ptr::null_mut();
};
let references: Vec<citefind::Reference> = match serde_json::from_str(references) {
Ok(references) => references,
Err(why) => {
set_error(why.to_string());
return ptr::null_mut();
}
};
match crate::merge_citation_reply(&references, reply) {
Ok(merged) => give_json(&merged),
Err(why) => {
set_error(why);
ptr::null_mut()
}
}
}
/// The series a title belongs to. Returns a JSON proposal, or NULL when the
/// title says nothing about a series — which is most titles, and not an
/// error: `catena_last_error` is NULL in that case.