get_ocr_pages()is listed in the pkgdown reference index, which the site build requires for every exported function.
- New
get_ocr_pages()returns a document's OCR markdown one page at a time, with each page's stored images embedded as<img>tags. Viewers such as ecoreview can call it instead of re-implementing the image lookup (#159).
- OCR images are matched to their placeholders by the image's stored
id, within each page.embed_images_in_markdown()matched by position, so with Mistral's document-wide image numbering most images were left as placeholders or showed the wrong image, inget_ocr_html_preview()too. It is replaced by the internalembed_page_images(). Placeholders with no stored image become their alt text (#159, #160). get_ocr_html_preview()no longer fails on single-line markdown, whichmarkdown::mark_html()treated as a file path.- HTTP 500-504 server errors are retried on the same model, like malformed output. ellmer already retries 429, 503, and 529, but not 500, 502, or 504, so a transient server error marked the document failed (#158).
- New
coalesce_metadataargument toprocess_documents()andprocess_single_document()sets how metadata results are saved. NULL (default) replaces the stored metadata on a forced re-run (force_reprocess_metadata), including clearing fields the new result leaves empty, and otherwise only fills fields that are still empty. TRUE or FALSE overrides this in either direction (#156). - Reviewer edits to document metadata are protected.
save_document()logs each metadata field whose value changes in a newdocument_editstable, and later metadata runs never overwrite those fields, whatevercoalesce_metadatasays. Existing databases gain the table on their next run (#156).
- A forced metadata re-run now clears fields whose new result is empty.
Previously every field was saved as
COALESCE(new, old), so an empty result kept the old value, and a clean re-run could keep an oldmetadata_log(#155). save_metadata_to_db()no longer has anoverwritemode that deleted the document and its records before updating them (#155).
- New
reasoning_effortargument toprocess_documents()andprocess_single_document()controls model thinking for every LLM step (metadata, extraction, refinement, and LLM deduplication). Set an effort level such as"low","medium", or"high"to turn thinking on (#153).
- Thinking is off by default. Since 0.1.23 (ellmer 0.5.0), Claude Sonnet 5
ran adaptive thinking at high effort on every call, which multiplied output
tokens and run time. Claude now gets
thinking: disabledunlessreasoning_effortis set, and Gemini keepsreasoning_tokens = 0. Other providers use their model default (#153).
- ecoextract now requires ellmer 0.5.0 or later. ellmer 0.4.x did not recognize Claude 5 models as supporting native structured output, so their output came back as a JSON string that failed to parse when the model's text contained unescaped quotes, backslashes, or control characters. On 0.5.0 the API returns a parsed object (#151).
- The retry for malformed structured output also covers jsonlite's "lexical error" messages (e.g. an unescaped quote inside a string), not only "parse error" (#151).
- Token usage is recorded for every LLM call, read from the API reply through
ellmer. The
documentstable gains<step>_input_tokens,<step>_output_tokens, and<step>_cached_input_tokenscolumns for the metadata, extraction, and refinement steps. Totals include retries, failed attempts, extraction reps, and LLM deduplication. Each attempt in the*_logaudit trail carries its own usage. Existing databases gain the columns on their next run; earlier rows stay NULL (#147). - The metadata schema's metadata object can have any name, not just
publication_metadata(#148).
- Metadata columns in the
documentstable come only from the metadata schema in use. A database created with a custom metadata schema no longer gets the thirteen bibliographic columns (title,authors,journal, ...). The default schema still creates them, and existing databases keep their columns (#148). export_db()exports the metadata schema's fields as document columns (the default schema addslanguage), andsimple = TRUEkeepsdocument_id,file_name, and the schema'sx-record-id-fields. It gains ametadata_schema_fileargument (#148).
-
export_db()keeps documents with no records, as one row with empty record columns. It previously used an inner join, so documents where extraction found nothing were missing from the export (#149). -
LLM calls whose structured output cannot be parsed (for example, stray markup after the JSON object) are retried on the same model before falling back to the next one. Content refusals are still not retried.
-
process_documents(document_id = ...)finds documents from any subdirectory of the project. Stored file paths are relative to the project root but were resolved against the working directory, so reprocessing by ID from elsewhere reported "file not found". -
refine_records()no longer reports "Refinement failed: object 'error_log' not found" when every record in a document is human-edited or deleted. The step is now markedskipped.
- Projects can supply their own metadata schema and prompt, via
ecoextract/metadata_schema.jsonandecoextract/metadata_prompt.mdor the newmetadata_schema_fileandmetadata_prompt_filearguments toprocess_documents(). Fields not already in thedocumentstable are added as columns (#138). - New
run_metadataargument toprocess_documents()skips the metadata step (#138). - Record IDs are built from the fields named in the metadata schema's
x-record-id-fields(metadata fields,document_id, orfile_name) instead of hardcoded author and year. The default schema keepsfirst_author_lastnameandpublication_year, so default IDs are unchanged (Smith_2020_1_r1) (#138).
- The default model is now
anthropic/claude-sonnet-5(wasanthropic/claude-sonnet-4-5) for all steps (#142). - A document's metadata now counts as present if any metadata schema field is populated. Previously title, first author, and year were all required, so documents missing one re-ran metadata on every pass.
- Missing record ID values become
Unknown(previously a missing year became the current year).
- Schemas sent to Anthropic and OpenAI now have
"additionalProperties": falseon every object instead of having it stripped, which Claude Sonnet 4.6 rejected with HTTP 400. Gemini schemas still have it removed (#140). - The default metadata schema lists every field in
required(with nullable types), so it stays within the structured-output limits of older models such as Claude Sonnet 4.6 and Haiku 4.5 (#142). - When every model fails, the error message links to the README's new Troubleshooting section (#142).
- Refinement now declares
record_idin the schema it sends, so models return it for each refined record. It was previously an undeclared extra field, which"additionalProperties": falsedisallows (#145).
- README and configuration guide cover custom metadata schemas and troubleshooting unexplained schema errors, including measured model limits (#142).
- Parallel worker completion messages now include
document_id.
process_documents()now acceptsdb_connalone — omitting bothpdf_pathanddocument_idprocesses all documents already in the database. Useful for re-running extraction on a database that was split from a larger one.
process_documents()withworkers > 1now passes a unique seed to each parallel worker viacrew'sseedargument. Previously, workers spawned simultaneously could inherit identical RNG state and generate colliding UUIDs, causing UNIQUE constraint failures onrecords.id.
process_documents()withforce_reprocess_extractionnow deletes existing unedited records for the document before re-extracting, instead of relying on dedup to filter them out. This prevents record accumulation across multiple force runs and eliminates the risk of UNIQUE constraint failures onrecords.id. Human-edited records (those with entries inrecord_edits) are preserved.
-
save_document()now correctly includesid(UUID) when inserting intorecord_edits. Since 0.1.13 the column isTEXT PRIMARY KEY(NOT NULL), so omitting it caused every cell edit followed by Verify to fail with a NOT NULL constraint error and roll back the entire save (#133). -
Extraction write failures are no longer silently swallowed. When
save_records_to_db()throws, the error message is now written intoextraction_status(e.g."Extraction failed: ...") rather than leaving the document marked"completed"with 0 records extracted (#134).
get_document_content()no longer throws"missing value where TRUE/FALSE needed"whendocument_contentisNULLin the database (returned asNAin R). Theis.null()check did not catchNA, causingNA == ""to evaluate toNAandif(NA)to error. Fixed by extracting the value first and checking withis.na()/nzchar().
-
migrate_ecoextract_database()now correctly upgrades databases whoserecordstable was created bydbWriteTablerather thaninit_ecoextract_database(). The previous approach patched the DDL string fromsqlite_master, which silently failed when column names were backtick-quoted orAUTOINCREMENTwas absent. The migration now builds the new DDL fromPRAGMA table_infoand usesALTER TABLE ... RENAME TO/CREATE TABLE/dbAppendTable/DROP TABLEinstead of DROP + CREATE +dbWriteTable, so the schema change is guaranteed regardless of how the original table was created. -
Records with
NULLid (possible when the original table was created withoutAUTOINCREMENT) now receive a fresh UUID during migration instead of remaining NULL. -
Schema migration warning is now issued from
get_records()(as a message, printed immediately) andsave_document()(as a hard error) rather than frominit_ecoextract_database(), which is only called when creating new databases. Old-schema databases opened for reading or writing are now always surfaced correctly.
-
records.idis nowTEXT PRIMARY KEY(UUID v4) instead ofINTEGER PRIMARY KEY AUTOINCREMENT. UUIDs are generated in R using base functions only (no new dependency). This makes records collision-free when merging databases from independent researchers. -
Added
migrate_ecoextract_database()to upgrade existing databases created before 0.1.13. The function recreatesrecordsandrecord_editswith TEXT ids, backfills UUIDs from a per-session integer-to-UUID map, and runs the entire operation in a single transaction. Safe to re-run. -
init_ecoextract_database()now warns when it detects the old integer-id schema, prompting the user to runmigrate_ecoextract_database(). -
All record insert paths (bulk extraction via
save_records_to_db()and single-row user additions viasave_document()) now generate UUIDs for rows that do not already have an id.
save_document()now assignsidafter inserting a user-added row, fixing infinite duplication on repeated verify clicks in databases created with the old schema whereidwas a plainINTEGERrather thanINTEGER PRIMARY KEY. After each insert,UPDATE records SET id = last_insert_rowid() WHERE rowid = last_insert_rowid()is executed; this is a no-op on new-schema databases whereidis already set by autoincrement.
save_document()no longer silently drops user-added rows when the document has zero LLM-extracted records. Previously the diff/insert block was guarded bynrow(original_df) > 0, which skipped the insert path whenoriginal_dfwas an empty tibble. Thenrow()guard is removed;diff_records()already handles a 0-roworiginal_dfcorrectly. Passingoriginal_df = NULLstill skips record changes as documented.
-
Fixed
document_id <- NAclobber inocr_document()that caused reprocessing viadocument_idto create a duplicate documents row when the file hash had changed (e.g. after opening and re-saving the PDF). -
process_documents(document_id = X, force_reprocess_ocr = TRUE)now returns a clear"file not found"status and leaves the database untouched when the stored file path no longer exists, instead of nullifyingocr_statusand failing mid-pipeline. A subsequent run withoutforce_reprocess_ocrwill use the stored OCR content.
-
PDF file paths are now stored relative to the project root (found by walking up from the PDF file's directory until a
.git,.Rproj,DESCRIPTION, or.heremarker is found). This makes paths portable across machines and when the project folder is moved. If no project root marker is found the absolute path is stored as a fallback — this is machine-specific and will break if the database is shared or the project folder is moved. -
When a document is identified by hash match but the file has moved to a new path,
file_pathin the database is immediately updated to reflect the current location.
process_documents(document_id = ...)now correctly reprocesses the specified document in-place instead of creating a duplicate row when the file's hash has changed since initial processing (e.g. after opening and re-saving the PDF). The originaldocument_idis passed through toprocess_single_document()and used directly, bypassing the hash lookup.
-
Extraction now uses a two-turn LLM conversation: turn 1 produces structured reasoning (capturing document analysis, organism identifiability decisions, and extraction choices), turn 2 uses that reasoning as context to extract structured records. This ensures reasoning causally precedes extraction rather than being a sibling field in the same structured output call, fixing divergence between stated reasoning and extracted records.
-
reasoningremoved from the extraction JSON schema — it is now always captured via the two-turn structure and stored in the database regardless of schema configuration. -
Content refusals on turn 1 are now correctly detected and logged as refusals rather than retried as stochastic empty-reasoning failures.
- Initial release.