sm_plot_landscape() now uses the same narrowly scoped older-R PCA
compatibility repair as clustering. Its Euclidean distance and automatic
thread defaults are unchanged; unrelated dependency errors still propagate.
Standardize the named v5 package logo across README, documentation pages and available application headers, with matching light and dark artwork.
The UMAP reducer now retries with base SVD initialisation when its PCA dependency raises the older-R NULL-input error. Other errors are still reported unchanged; successful UMAP calls retain their existing defaults.
Version metadata aligned across DESCRIPTION, CITATION, CITATION.cff and .zenodo.json; corpus print shows refresh times as UTC dates; ?sm_corpus documents the column schema.
sm_example_corpus() preserves the session random-number state, including when no seed existed. The package engine of sm_fetch_crossref() restores the previous crossref_email environment variable after success or failure.
Clustering checks for the optional UMAP dependency only when it actually uses UMAP, so PCA and unreduced density clustering work without it. Examples, tests and trajectory plots guard their optional dependencies. sm_export_zip() creates and cleans its temporary directory using base R.
sm_audit_geographic() counts works, not authorship rows. A work counts once in each country, region or income tier of its authors, and with the new method = "fractional" it splits one unit among them. Its citations count once per group, coverage is the share of works with a known group, and the Gini coefficient uses citations under weight = "citations". Results change for corpora with multi-author works or authors with several addresses. The distribution gains credit and weighted_citations, appended after its existing columns (group, count, citations, pct).
sm_metric_collab_index() returns NA instead of 0 and FALSE when no author of a work has a country code (n_countries, is_international) or an institution id (n_institutions). Web of Science and Scopus corpora without country data no longer report an international collaboration rate of 0%.
New sm_oa_statuses() lists the open-access statuses (diamond, gold, hybrid, bronze, green, open, closed). sm_audit_oa(), sm_summary_works() and sm_summary_period() now use one rule: every status except closed is open, and missing or unrecognised values are unknown. The audit counts Semantic Scholar's open and OpenAlex's diamond as open, and the summaries no longer count values such as "Closed" as open. The distribution of sm_audit_oa() gains is_open. sm_read_dimensions(), sm_read_lens(), sm_corpus() and sm_corpus_from_tables() map export spellings to these statuses; the last two say so in a message. sm_read_lens() takes the status from the open-access colour or status column and no longer from whichever column name starts with "Open Access".
sm_filter_works(oa_only = TRUE) keeps diamond and other open-access works in any recognised spelling; it used to drop every status except gold, green, hybrid and bronze written in lower case.
sm_metric_summary() shares tied citation counts fractionally when it computes pp_top10, so ties no longer push the value towards 1. Measured against the corpus itself, pp_top10 now equals top_pct by construction. The new top_reference argument takes a reference distribution of citation counts (optionally by year and field) or "openalex" for OpenAlex's top-percentile flags, which makes the value informative.
Printing a chat response, sm_chat_render() and sm_cite_corpus() show their text as it is. Before, braces in an answer, a question, a cited title or a citation were run as R code: text such as "{RCT, cohort}" stopped the printing, "{T}he" printed as "TRUEhe", and sm_chat_render(format = "quarto") and sm_cite_corpus(style = "bibtex") always failed. The line breaks of BibTeX, YAML and Markdown output are now kept. When a request of sm_fetch_arxiv(), sm_fetch_orcid(), sm_fetch_overton(), or of the rcrossref or rentrez engine of sm_fetch_crossref() or sm_fetch_pubmed(), fails, the error shows the original error text as written, also when it contains braces.
sm_unlock() and the lock check of the functions that modify a corpus show a lock reason that contains braces as written instead of running it as R code.
sm_chat_render(format = "html") escapes &, <, > and quotes in the answer, the question, the model name and the cited titles, so text from the corpus or from the answer can no longer add markup or scripts to the page.
sm_screen_against_question(), sm_chat() and sm_cluster_label() with a chat provider (llm_provider) send every request in a fresh copy of the chat object, with their instructions as its system prompt. Earlier batches, clusters and questions are no longer sent again, and the chat object you pass is left unchanged. Before, the instructions went as a second user message, which providers can reject, and every request carried the whole earlier conversation. A system prompt or conversation already set on the chat object is not used. An object that is not a chat object is refused before any request.
sm_chat() records the model name of the chat object instead of "unknown". Its arguments max_tokens and temperature, which had no effect, are deprecated: set them on the chat object instead.
sm_screen_against_question() no longer creates a default chat provider when none is given; pass the chat object that may receive the titles and abstracts. It reports which model receives them, and its screening rows record that model in the new column model, which also enters the corpus hash.
sm_screen_against_question() sends the works as JSON data, asks for the work ID with every decision and matches decisions to works by that ID instead of by their order in the answer. A skipped, repeated or reordered item no longer moves decisions to other works; such works are recorded as uncertain with source "llm_incomplete" or "llm_misaligned". Answers in a code fence, over several lines, as a single object or as one object per line are read, and an answer that cannot be read marks only its own batch as uncertain instead of stopping the run. A corpus with repeated work IDs is refused before anything is sent.
sm_screen_against_question() runs title-abstract before full-text, and screens at full-text only the works whose latest title-abstract decision includes them, or leaves them uncertain with include_uncertain = TRUE; decisions of other stages, such as regex screening, no longer count. Works whose request failed or whose answer could not be read are screened again on the next run, their old rows are replaced, they are no longer carried forward, and a warning counts them. Screening decisions use one vocabulary, "include", "exclude" and "uncertain". "unclear" is read as "uncertain", and sm_example_corpus(with_screening = TRUE) now writes "uncertain", which changes the hash of that corpus. The full-text stage screens titles and abstracts, as is now documented.
sm_screen_prisma() uses the stage names the package writes by default ("identification", "title-abstract", "full-text", "inclusion"), so its default flow shows the title-abstract and full-text exclusions instead of none. It counts works instead of decision rows: per stage it takes each work's latest decision and only works of the corpus that are still in the flow. Repeated decisions therefore no longer count twice, and the remaining count can no longer turn negative. It warns when screening decisions of a stage are left out of the flow and when decisions concern works outside the corpus. The records that sm_corpus_for_question() found before removing duplicates enter at identification (new argument n_identified).
sm_export_rayyan() and sm_export_covidence() write each work's ID (RIS ID and a note) and leave out missing fields instead of writing "NA". sm_import_rayyan() returns one row per record of the file. It takes the work ID from that note, from a column you name, or from the DOI of a corpus you pass (new arguments corpus and id_col); it reads the decision from a decision column or from the inclusion notes of the export, maps labels such as "Included", "Excluded" and "Maybe" to the package vocabulary, and warns about records it cannot match.
sm_cluster_label() with a chat provider (llm_provider) sends the sampled titles as JSON data and gains seed, which makes the sample of 20 titles per cluster reproducible without changing the session's random numbers.
sm_audit_gender(method = "genderize") caches only answers it received: failed lookups, including those an earlier version stored as unknown, are retried on the next run. Names are sent ten per request, and a rate limit stops the lookups with a warning that gives the reset time and the number of names not looked up (new element n_names_failed).
sm_audit_funding() reports failed lookups (n_works_failed) and works without an identifier (n_works_no_id), and computes coverage and the funders' pct over the works it could check (NA when it could check none); before, both counted as unfunded. It no longer needs rcrossref or openalexR, sends OPENALEX_API_KEY and SCIMAPR_MAILTO (new api_key and mailto arguments), and stops further lookups after a rate limit.
The Crossref, Semantic Scholar, bioRxiv, PubMed and OpenAlex fetchers retry transient failures (HTTP 5xx, network errors, short HTTP 429 waits). A request that still fails after earlier pages arrived ends in an error of class scimapR_fetch_incomplete that carries the works fetched so far (rlang::last_error()$partial). sm_fetch_crossref() pages with a cursor, so n_max above 10,000 works; sm_fetch_semantic_scholar() caps relevance search at 1,000 results with a warning; Crossref records with an empty date or an organisation author no longer stop the fetch or lose the author.
sm_refresh() marks a work as refreshed, and adds a provenance row, only when new data for it arrived. Works whose request failed, that the source did not return, or whose source cannot be re-queried stay stale and are counted in a warning. metadata$last_refresh moves only when works were refreshed and none failed. OpenAlex and Crossref refreshes are sent in batches; OpenAlex requests use OPENALEX_API_KEY and Crossref requests SCIMAPR_MAILTO (new api_key and mailto arguments), and a rate limit stops further requests at once. The default what is now c("citations", "oa_status"), because "new_refs" and "altmetric" were never implemented; asking for them gives a warning.
Repeated sm_refresh() calls query each work once, from the source that first fetched it, and label new provenance rows <source>_refresh without adding a suffix per run; rows written by enrichers no longer start a refresh.
sm_refresh(sources =) with an unknown source reports the unknown names and the valid sources instead of an internal formatting error.
sm_enrich_retraction() counts a work as checked only when OpenAlex returned its record or a Crossref retraction notice names it: failed or rate-limited requests leave works unchecked and without a provenance row, and are reported in a warning, or in an error when nothing could be checked. It sends OPENALEX_API_KEY (new api_key and mailto arguments), and its Crossref cross-check now looks for retraction notices that update the work's DOI, in batches and for corpora of any size. sm_enrich_concepts(source = "openalex") sends the key (new api_key argument) and reports failed lookups.
sm_read_bib() removes BibTeX protective braces and decodes common LaTeX
accents and escapes, so Web of Science BibTeX exports give plain DOIs, titles,
journals and keywords. It splits authors only at an and outside braces, so a
braced corporate author stays one author, reads quoted values that contain
braced accents, and takes the year from a biblatex date field when year is
missing.
sm_read_wos() and sm_read_auto() read Web of Science tab-delimited
exports, which they used to read as one empty record. sm_read_wos(engine = "bibliometrix") stops with an error on such files. A WoS file whose records
carry no title, accession number or author is now an error, and such records
among others are dropped with a warning.
sm_read_ris(), and sm_read_cochrane() for RIS files, read years from RIS
dates such as 2019/03/01/ or 2020/// in PY, Y1 or DA, instead of
returning NA with a coercion warning.
sm_read_auto() tells EndNote XML from PubMed XML by the root element, so an
EndNote library of PubMed records is no longer read as an empty PubMed file.
Unrecognised XML, MEDLINE (.nbib) and Refer (.enw) files now stop with a
clear error instead of failing in the XML parser or giving an empty corpus,
and .nbib or .enw files that hold RIS records are read as RIS.
sm_self_citation(), the self-corrected h-, g- and m-indices,
sm_metric_disruption() and sm_metric_novelty() now work on corpora
built by the readers and fetchers. Before, they found no linked
references and returned empty or uncorrected results. Their warnings
now point to sm_link_references() instead of
sm_enrich_opencitations(), which did not link references.
sm_network_coupling() and sm_network_cocitation() now count shared
cited references outside the corpus too, by DOI, OpenAlex id or
reference text. This changes their default results for corpora with
unlinked references. Co-citation nodes gain in_corpus;
linked_only = TRUE keeps the earlier behaviour. Citation networks,
and the indicators that need linked references, warn when none are
linked.
sm_enrich_opencitations() uses the Index API v2 to add each work's
outgoing references and link those that cite corpus works by DOI or
OpenAlex id. Before, it stored the works citing a work as that work's
own references, reversing citation direction. It still sets
cited_by_count to the OpenCitations count. New reference indices
continue after existing ones, and repeated enrichment skips existing
cited identifiers, regardless of DOI case.
sm_metric_disruption() no longer counts the focal work, or works
published before it, among the works that cite only its references.
Same-year works and works with missing years remain eligible. Uncited
works now get NA instead of 0, and repeated or self-directed links
are ignored. Existing CD index values can change.
sm_export_gephi(), sm_export_vosviewer() and
sm_export_cytoscape() export the edges of the network. Before, they
wrote the node table in place of the edges, so the files had no usable
links.
sm_export_gephi() escapes labels and marks directed networks as
directed. sm_export_vosviewer() now writes a map file of the node
ids and labels (map_path) and a network file without a header, with
numeric ids and positive link strengths.
Coupling and co-citation weights count each cited item once per work, and citation networks drop self-citations of a work and repeated links. Before, repeated reference rows raised the weights.
References now link to the works of the corpus they cite: the Web of
Science, bibliometrix, OpenAlex, Crossref and Semantic Scholar readers
and fetchers, sm_bind_corpora() and sm_dedupe() set
references$cited_work_id from the cited DOI or OpenAlex id. New
sm_link_references() links corpora built with sm_corpus() or
sm_corpus_from_tables(), or saved earlier. Cited references in the
Web of Science form DOI [a, b] now get a DOI. Both change the
references, and so the corpus hash, of corpora read from files whose
works cite each other.
Functions that modify a corpus now refuse locked inputs, including
merging, deduplication, materialisation, citation maturity, enrichers,
embeddings, clustering, labels, affiliation matching and screening.
Subsets stay locked; merged corpora are unlocked. sm_lock() records
a content hash, and sm_certificate() and sm_validate() report later
changes to that content. Printing continues to show only the lock status.
Subsets ([, sm_filter_works(), sm_query()) and sm_dedupe() keep
only the authors, institutions and sources their works refer to, so
printing, summaries, citations and certificates count the subset. Its
corpus hash changes accordingly.
sm_audit_gender() and sm_summary_authors() on a filtered corpus count
only its contributing authors. The genderize and ssa methods look up
only their names.
sm_cluster_kmeans(), sm_cluster_hdbscan(), sm_cluster_leiden() and
sm_network_semantic() stop with an error naming repeated work IDs,
embedding row names or network nodes instead of adding duplicate works.
sm_dedupe() keeps one embedding row per retained work, with a warning
when duplicate embedding rows are removed.
sm_dedupe(by =) now uses the named works columns, such as "pmid" or
c("title", "year"); previously it ignored by and always used DOI.
Missing and empty values never count as duplicates, DOIs and titles are
normalised, and retained works keep their input order.
sm_materialise() skips enrichment rows with missing keys with a warning
and leaves corpus rows with missing keys unmatched. Previously, one row
could overwrite multiple references or keywords. The default reference
key is now (work_id, ref_index), replacing (work_id, cited_work_id).
Ambiguous row keys in works, authors, institutions, sources, references
or concepts now cause an error; group keys in authorships, provenance
and screening may still repeat.
sm_bind_corpora() and sm_build_corpus() give the second corpus new
work, author, source and institution IDs where the first already uses
them, and rewrite every table that refers to them. Previously, separately
read files merged into colliding works and credited papers to the wrong
authors and journals. Identical lookup records share one ID. The new
verbose argument reports re-keying, and incompatible embedding widths
or one-sided embeddings now warn when dropped. The existing mix_models
rule for different recorded models remains. sm_validate() also checks
author, source, institution and embedding links and duplicate IDs.
New synthetic example files shaped like a Web of Science export of three
invented military-medical units and their civilian partners:
example_realuse_corpus.rds (150 works), example_realuse_reference.csv
(a publication-tracker reference list) and
example_realuse_affiliation_dict.csv (an affiliation dictionary). See
the "Real-use fixture" section of ?sm_example_files.
The examples of sm_audit_funding() and sm_audit_summary() run
offline: they no longer send one Crossref request per DOI of the example
corpus when R CMD check --as-cran runs \donttest{} code. The live
lookup is shown in an example that runs only in interactive sessions.
sm_audit_summary() no longer sends web requests by default. Its new
online argument controls the funding audit, which looks up every DOI on
Crossref: with the default online = FALSE that audit is reported as not
run unless a precomputed sm_audit_funding() result is passed, and
online = TRUE runs it as before.
sm_certificate(), sm_cite_corpus() and citation-maturity cut-offs
(sm_citation_maturity(), sm_its(outcome = "cnci")) accept numeric,
character, Date and POSIXct timestamps in metadata$build_date and
metadata$last_refresh. Numbers are read as seconds since 1970-01-01 UTC,
as corpora restored from tables store them, and implausible numbers (such
as milliseconds) are ignored with a message. Text is read as ISO 8601 (a
date, or a date and time with an optional Z or +hh, +hhmm or
+hh:mm offset); other text counts as missing. Certificates write their
timestamps in UTC, for example 2026-10-17T11:45:00Z, still read the
older form, and print the creation time in UTC.
sm_metric_fnci() gains min_cell and by_type (opt-in fallback
baselines) and informs when many works sit in tiny cells;
sm_its(outcome = "cnci") and sm_metric_summary(metric = "cnci") show
that message too. With min_cell, a work whose field-year cell is too
small falls back to its field in the neighbouring years and then to its
year, field may list several ;-separated categories, and the result
reports n_cell and baseline. by_type = TRUE gives articles and
reviews baselines of their own.
sm_metric_fnci(classification = "manual") reports a factor field by
its labels instead of its integer codes.
sm_metric_fnci(classification = "mesh") never uses a MeSH check tag
(such as Humans, Female or Adult) or a descriptor of study design or
statistics (such as Retrospective Studies, Risk Factors or Treatment
Outcome) as a work's field. sm_fetch_pubmed(), sm_read_pubmed_xml()
and sm_enrich_concepts(source = "mesh") now record each MeSH heading's
major-topic flag as its concept score (1 for a major topic, 0
otherwise; before, always NA), so the field is a major topic of the
work. Headings without scores, as from other sources, give the work's
heading that is most frequent in the corpus instead of the first one
listed, with a message.
sm_cluster_evolution(link_by = "cluster_id") follows a global clustering
across windows; snapshots gain share. Each cluster present in two
consecutive windows gives one transition, except the noise cluster named
by the new noise argument (default 0L).
sm_affiliation_match() matches dictionary patterns in Unicode mode, so
\w and \b work on non-ASCII names; new sm_affiliation_unmatched()
lists unmatched affiliation strings. Unicode mode is the new default, for
the postcode signal too: \w, \b, \d, \s and POSIX classes such as
[[:alpha:]] now treat umlauts, accented letters and other non-ASCII
letters, digits and spaces like ASCII ones. A pattern can stop matching
if it used \W, \S, \D or \B to stand for a non-ASCII letter, or
if it relied on \b between an ASCII and a non-ASCII letter: the shipped
pattern \bnato\b no longer matches "NATO" directly followed by an
umlaut. The new argument unicode = FALSE restores the earlier
ASCII-only matching. sm_attribute_institution() has no such argument
and matches in Unicode mode when it runs sm_affiliation_match() itself;
the Details of ?sm_affiliation_match show how to get its earlier
results.
sm_screen_regex() matches its terms as written, case-insensitively and
in Unicode mode like sm_affiliation_match(), so a term can stop
matching in the same ways (new argument unicode, default TRUE). It no
longer lowercases the patterns, which turned \W, \S, \D and \B
into \w, \s, \d and \b, and a corpus with a single work now gets
one screening row instead of several.
sm_its() no longer fails when a year has no finite outcome, sm_did()
no longer fits silently on fewer years, and sm_synth() no longer passes
such years to tidysynth; these years are dropped with a warning and
reported (dropped_years). sm_did() drops them per group, and
sm_synth() for every unit so that its panel stays balanced. For
outcome = "cnci", works with an infinite impact value are left out of
the yearly mean, and a year without usable impact counts as dropped even
when it is citation-immature, so dropped_years and provisional_years
never share a year.
The confidence intervals of sm_its() and sm_did() use the t
distribution with the residual degrees of freedom when the model
estimates its dispersion, as their p-values already did: the default
Gaussian fits of sm_did() and of sm_its() for "share_q1", "cnci"
and "leadership", and quasi families. These intervals are wider than
before and agree with the p-values. Poisson fits (the sm_its() default
for "count") keep normal intervals and now warn when the counts are
overdispersed, suggesting family = "quasipoisson".
With outcome = "count", sm_its(), sm_did() and sm_synth() count a
year without works as 0, over every year from the first to the last
publication year of the corpus, instead of leaving the year out; filter
the corpus to the analysis window first. sm_synth() aborts when a unit
has no works.
sm_its() checks the years that remain after all exclusions (years
without a usable value and, for "cnci", citation-immature years)
instead of before the citation-immature cut. It needs at least 4 years, 2
or more before intervention_year and 2 or more from it on, and aborts
otherwise, or when a level or slope change cannot be estimated, instead
of silently fitting a model without those terms. On exactly 4 years a
Gaussian or quasi-family fit has no residual degrees of freedom, and
sm_its() now warns that its standard errors, intervals and p-values
are NaN. sm_did() needs 4 years per group, with years before and
from intervention_year, and sm_synth() 4 panel years, 2 of them
before it.
sm_its(), sm_did() and sm_synth() check intervention_year the
same way and abort unless it is one whole year, given as a number or as
text such as "2020". Before, sm_did() and sm_synth() truncated a
year such as 2020.5 and read TRUE as the year 1, and all three read a
factor or a date by its internal number.
Concept vocabularies are now a controlled set (sm_concept_vocabularies());
sm_to_bibliometrix() DE and keyword co-word networks work for file-based
corpora. sm_corpus() and sm_corpus_from_tables() rename legacy and
common spellings such as "keywords-author", "keyword" or "MEDLINE"
to these names, with a message, and the functions that select concepts by
vocabulary read every one of these spellings. Values that name no known
vocabulary are kept as written and listed in the same message, so a
corpus built from such tables now reports them each time; name a
vocabulary of your own "custom". DE holds each work's author keywords;
a work without any falls back to its OpenAlex concepts, so corpora built
from OpenAlex keep their DE, and arXiv and bioRxiv categories stay out of
it as before. sm_network_coword() gains vocabulary:
field = "keywords" uses the author keywords by default and, in a corpus
without any, the indexer keywords (such as Keywords Plus or MeSH) with a
message; a corpus with neither gives an empty network, now with a
message. With field = "concepts", vocabulary keeps only the
vocabularies named.
sm_read_wos() reads the Web of Science categories (WC) into the new
vocabulary wos-category; wos-subject-category keeps the research areas
(SC). Corpora read from Web of Science files therefore have more concept
rows, which changes the results of functions that use every concept
whatever its vocabulary: sm_network_coword(field = "concepts"),
sm_plot_thematic_map(), sm_author_trajectory() and the corpus hash
(sm_hash_corpus()). sm_author_trajectory() now counts a concept name
once per work in its dominant topics and novelty (before, a name that a
work had in two vocabularies counted twice), so a category with the same
name as a research area of the work leaves the trajectory unchanged.
sm_to_bibliometrix() exports C1, RP, ID, WC, SC, UT, SN/EI, J9/JI, EM,
AF, one AU entry per author, CR in Web of Science form and DB = "ISI"
for corpora from Web of Science, so bibliometrix::histNetwork() and
bibliometrix::rpys() run; C1 round-trips through as_sm_corpus().
Every tag column is present, NA where the corpus has no data. New
arguments: keep_extra = FALSE drops the columns without a field tag,
and db sets DB. sm_read_wos() now reads the ISSNs and journal
abbreviations (SN, EI, J9, JI) into sources (issn_l, issn and the
new columns abbreviation and iso_abbreviation), so ISSN lookups now
work for corpora read from Web of Science files. It also keeps each
author name as the file writes it (such as "Garcia Lopez, M") in
authors$display_name_alternatives, so the export keeps surnames of
several words whole, and cited-reference DOIs lose a trailing period.
Because of these changes the corpus hash (sm_hash_corpus()) of corpora
read from Web of Science files changes too.
sm_to_bibliometrix() now upper-cases the text fields that bibliometrix
keeps upper-case (as convert2df() does); DI stays as stored. Its
default output changes in a few more ways: AU holds names as
SURNAME INITIALS (such as VALDEZ P) instead of the stored display
names, SR is built as convert2df() builds it (first author, year and
journal abbreviation, made unique) and gives the row names, NR counts
the items of CR instead of the reference rows, DE separates its terms
with ; , and the tag columns come first. AU_CO is no longer written,
so bibliometrix reads the countries from C1
(bibliometrix::metaTagExtraction(M, "AU_CO")) as it does for
convert2df() data. With the default keep_extra = TRUE the other
columns stay, and the ISO country codes that AU_CO held are in the new
column country_code.
The "bibliometrix" and "auto" engines of the file readers and
as_sm_corpus() for bibliometrix data frames now share one conversion,
which also reads the cited references (CR, with the DOI of each reference
that has one), the sources (SO, SN, EI, J9, JI), the keywords and
categories (DE, ID, WC, SC), the Web of Science accession numbers (UT)
and the PubMed IDs; before, these corpora had no references, sources or
concepts. Authors keep their full names from AF and get one authorship
row per address, linked through the names in C1, and titles and
abstracts keep the case of the file. Authors are now told apart by
their full names in AF instead of their AU form, so author counts can
change; an entry with initials only, or without AF, belongs to the
author with the same surname and initials when there is just one.
as_sm_corpus() labels the provenance of Web of Science data
(DB = "ISI") "wos".
sm_coverage_audit(by =) accepts any reference or works column;
corpus-side affiliation uses institution matches before country codes.
A name is either one of the named dimensions "year", "source" and
"affiliation" or a column of the reference data frame (matched
case-insensitively) or of works; a dimension found on one side only
gets NA for the other side, and a name found on neither side is an
error listing the available columns. The affiliation levels of a corpus
come from the first of institution_match, institution_name,
institution_id and country_code that has a value, with a message
naming the column on each side and a warning when a side falls back to
country codes. A work counts in every institution of its authors, so
per-level counts can add up to more than the total; the overall recall
and precision count each record once. In a data-frame reference,
"affiliation" now takes an affiliation, affiliations or
institution column before a country or country_code column, and
"source" a source name before source_id, whatever their order, and
the values of each row stay with that row.
sm_coverage_audit() and sm_reconcile() identify the rows of a data
frame by its UT column (Web of Science accession numbers) or an id,
work_id, reference_id or ref_id column only when its values are
unique and none is missing; otherwise rows are identified by their
position (R000001, ...), with a warning. A column named ID next to
Web of Science field tags holds the Keywords Plus and is never used.
Before, missing or repeated identifiers, such as those of the ID column
that sm_to_bibliometrix() writes, made the recall, the breakdowns,
n_reference_only and matches$reference_id disagree.
sm_coverage_audit() looks up each reference row on its own for recall,
so an item that two rows report (one paper reported by two units) counts
as found for both instead of only for the first; precision still pairs
each corpus record with at most one reference row. n_matched counts the
reference rows found and the new element n_corpus_matched the corpus
records paired, which print() shows for precision and summary() adds
as its last column; the two differ only when reference rows share a
corpus record. The new element reference_matches shows for each
reference row the corpus record it was found in, as matches does for
the corpus records. When a DOI repeats on both sides, its records now
pair in order instead of leaving the later ones to the title fallback.
The title fallback of sm_coverage_audit() and sm_reconcile() pairs
two records only when their publication years differ by at most one
(or a year is missing) and never when both have a DOI and the DOIs
differ, so serial titles such as annual reports and generic titles such
as "Editorial" no longer match across years. A Date or date-time year
column of a data frame counts by its calendar year, and a year outside
1000-2999 (such as a day count) as missing.
Title matching in sm_coverage_audit() and sm_reconcile(), and the ROR
lookup of sm_attribute_institution(), keep the letters and digits of
every script when they normalise text. Before, they dropped every
non-ASCII letter, so titles in Cyrillic, Greek or other non-Latin scripts
never matched and words with umlauts or accents lost letters.
sm_count(level = "institution") no longer applies the shipped
dictionary sm_affiliation_dict on its own, which credited any
"military hospital" or "army hospital" to "Bundeswehr Hospital". This
changes its default counts: each authorship row now takes the first of
institution_match, institution_name and institution_id (named by
corpus$institutions when listed) that has a value in that row, and
otherwise its raw affiliation string, with a warning. Before, one column
served every row, so the rows without a value in it were left out, and
entity_id was the institution_id whenever any row had one; now it is
the label, except for rows labelled through institution_id. After
sm_attribute_institution(), which keeps institution_match, the
counts therefore follow the dictionary matches instead of their roll-up
to ROR ids and names; to count by the vocabulary, replace
institution_match by institution_name where that is set (see
?sm_count). Run sm_affiliation_match() with a dictionary of your
institutions first to count them.
The shipped dictionary sm_affiliation_dict no longer credits a bare
"military hospital", "army hospital" or "armed forces hospital", which
can be a hospital of any country, to "Bundeswehr Hospital". Its rows for
that hospital now need "Bundeswehr", "German Armed Forces", "German
Army" or "German Military" before "hospital", or the German name, so
sm_affiliation_match() with its default patterns, and
sm_attribute_institution() on a corpus that was not matched first,
leave other military hospitals unmatched; an English name with
"Bundeswehr", such as "Bundeswehr Hospital", now matches as well.
The WoS reader (both engines), the bibliometrix import and both OpenAlex
readers take the corresponding author from the RP field or the OpenAlex
is_corresponding flag instead of assuming the first author; the
readers of sources without this information record it as unknown
(NA). sm_audit_geographic(weight = "corresponding") and the gender
audit now find corresponding authors (they used isTRUE() on a
column). RP is read in any case, with the older "(reprint author)" and
with its names on one line or several, in both engines; namesakes with
the same surname and initials are told apart by the address RP gives,
and stay unknown when it does not tell them apart. A work whose RP
names none of its authors counts as unknown, and so does an OpenAlex
work whose authors are all marked false unless its
corresponding_author_ids lists one of them. This changes default
results: sm_read_lens(), sm_read_ris(), sm_read_scopus(),
sm_read_bib(), sm_read_pubmed_xml(), sm_fetch_pubmed(),
sm_fetch_crossref() and the other readers and fetchers without
corresponding-author data now give NA instead of TRUE for the first
author, so sm_to_bibliometrix() exports RP = NA for their corpora,
and sm_audit_geographic(weight = "corresponding") reports the
countries of the corresponding authors instead of an empty table with
coverage 0.
Leadership outcomes can be restricted to focal institutions and author
roles (institutions, lead_role; institution_aware in sm_did() and
sm_synth()); works with an unknown corresponding author, and works
without any authorship rows (before counted as not led), leave the
default leadership denominator. A work counts as led when an authorship
in the role ("corresponding", "first", "last", "first_or_last" or
"any_lead") carries one of the institution labels in
institution_match, institution_name or institution_id, or the name
of that id in corpus$institutions; a label that no authorship carries
gives a warning naming the nearest labels. The denominator change
applies to sm_its(), sm_did() and sm_synth() alike and changes
their default "leadership" results for corpora with unknown
corresponding authors, which now include most corpora from readers
other than the WoS and OpenAlex readers: a year without a known
corresponding author is dropped with a warning, and when no work has
one the functions abort and suggest lead_role = "first" or "last"
with focal institutions. The print methods state the definition used,
which the results keep in leadership.
sm_read_wos() links the addresses of C1 to the authors by their whole
names, as the bibliometrix engine does: the full name of AF, else the
surname and initials, else the surname and first initial. It now writes
one authorship row per author and address. Before, it gave each author
the first C1 line whose bracket contained the author's surname anywhere,
so a short surname such as "He" or "Li" took the address of "Chen" or
"Lin", an author with several addresses kept only the first, and lines
without names linked nobody. In both engines and in as_sm_corpus(),
an address that C1 lists without names, as older records do, now
belongs to every author of the record, and the bibliometrix engine keeps
such a line apart from the address before it. Authorship rows,
address-based counts and the corpus hash of corpora read from Web of
Science files change.
sm_fetch_biorxiv() takes the corresponding author from the
author_corresponding field of the bioRxiv API, and
sm_read_dimensions() from a Corresponding Authors column, matching the
names to the authors by surname and given names or initials; a name
that fits two authors equally leaves both unknown.
sm_fetch_orcid() records the ORCID holder's position and
corresponding-author flag as unknown (NA); before, the holder was
first and corresponding author of every work, which counted as first
authorship in the geographic and gender audits.
sm_did() reports the works that belong to both the treated and the
control group, such as works co-authored by both, which pull the two
series together and the estimate toward 0: with a warning when the new
argument shared is not set, and in the new element shared_works.
shared = "exclude" leaves them out of both series and "lead" gives
each to the group of its lead author (lead_role); the default
"keep" keeps them in both, as before. sm_did() aborts when treated
and control share a label. sm_did() and sm_synth() gain lag and,
for outcome = "cnci", now leave out the citation-immature years as
sm_its() does and list them in provisional_years, which changes
their default "cnci" results.
sm_attribute_institution() keeps the institution_id and
institution_name of the authorships it does not match, such as the
OpenAlex institution ids of a fetched corpus. Before, it set them to
NA, so institution counts, the institution h-index and the regional
audit lost every institution outside the vocabulary.
sm_summary_authors(), sm_summary_period() and the position breakdown
of sm_audit_gender() count each author once per work, however many
address rows the author has in it, as sm_metric_collab_index() does.
Before, an author with two address rows counted twice, so a
single-author work did not count as single-authored and the authors per
work disagreed with sm_metric_collab_index().
OpenAlex fetches and sm_read_openalex_json() keep FWCI, citation
percentiles and yearly citation counts (new corpus element
citations_by_year); new sm_enrich_openalex_impact() adds them to any
corpus by identifier and stops cleanly on HTTP 429; new
sm_citation_window(). The works gain the columns fwci,
citation_percentile, is_top_10pct, is_top_1pct and
citations_as_of (the date of these values); the openalexR engine has
no percentile. sm_fetch_openalex() now stops with the same message on
HTTP 429, naming the reset time, the remaining budget and the
OPENALEX_API_KEY setting, instead of a generic error (native engine) or
an empty corpus (openalexR engine), and its openalexR engine now uses
OPENALEX_API_KEY too and needs openalexR 3.1.1 or later.
sm_enrich_openalex_impact() sends identifiers only, joins nothing when
a request fails, and records its step in provenance with the source
"openalex_impact" and the kind "enrichment", which
sm_rebuild_from_cert() never replays; certificates record the kind of
each step. sm_corpus(), sm_corpus_from_tables(), subsetting,
sm_bind_corpora(), sm_dedupe(), sm_export_csv(), sm_validate()
and sm_hash_corpus() carry citations_by_year; corpora without it keep
their hash.
sm_metric_summary(metric = "cnci") resolves impact like sm_its() and
reports its source as an attribute; both now use works$fwci when
works$cnci is absent (ahead of other impact columns and
corpus$metrics), which changes sm_its(), sm_did() and sm_synth()
results with outcome = "cnci" for such corpora. The attribute
impact_source is NA on the n = 0 row, which a corpus without usable
impact data still gives; a message names the source for "cnci" and the
new metric = "fwci", and the results of sm_its(), sm_did() and
sm_synth() keep it as impact_source and print it. sm_its(),
sm_did(), sm_synth() and sm_metric_summary() gain min_cell,
passed to sm_metric_fnci() when they derive impact from citation
counts (default NULL, as before). The error for a corpus without impact
data has the class scimapR_no_impact_data.
sm_fetch_openalex() and sm_read_openalex_json() fill
authorships$raw_affiliation from OpenAlex's raw_affiliation_strings
(joined with "; "), else from affiliations[].raw_affiliation_string,
else from the institution names. Before, sm_fetch_openalex() read a
field OpenAlex no longer has, so raw_affiliation was NA for every
fetched authorship and sm_affiliation_match() matched nothing, and
sm_read_openalex_json() kept only the name of the first institution,
without the department and unit text that dictionaries match.
sm_read_openalex_json() links works to their source (works$source_id).
Before, it was NA for every work, so source counts, the source plots
and h-index, ISSN lookups in sm_coverage_audit() and SO in
sm_to_bibliometrix() came out empty. Sources with the same name but
different OpenAlex ids now stay apart.
sm_read_openalex_json() reads a saved API response (an object whose
results hold the works, compact or pretty-printed), a single work
object and newline-delimited files whose lines are responses; before, a
saved response gave one empty work or a parse error. It also reads
abstracts from abstract_inverted_index when a record has no plain
abstract; before, such a record stopped the reader with an error.
sm_fetch_openalex() stores ORCIDs without the https://orcid.org/
prefix, as the other readers do, and sm_author_trajectory(orcid =)
accepts an ORCID in either form, so sm_researcher_profile() finds its
researcher; before, it aborted with "No author found with ORCID" for
every researcher with 5 or more works. sm_researcher_profile() gains
n_max (default 200, as before).
Timestamps given as ISO 8601 text with more than three decimals of a
second, such as 2026-10-01T04:12:33.123456 (the form of OpenAlex's
updated_date), are read, for example in metadata$last_refresh;
before, they counted as missing.
sm_embed_works() with the default model specter2 loads the SPECTER2
adapter through the Python adapters package as the model card
describes; it previously failed in SentenceTransformer(). This model
needs the Python packages torch, transformers and adapters
(install_deps = TRUE installs torch and adapters), the other models
need sentence-transformers as before, and the error for a missing
package names it. A model that does not load now stops with an error
that names the model and gives the Python error.
sm_embed_works() gains multilingual models (multilingual-mpnet,
multilingual-e5), informs when an English-only model meets a
multilingual corpus, warns about works without text, and records the
model in the corpus metadata and certificate. Texts for
multilingual-e5 start with "query: ", the prefix its model card gives
for clustering. The message appears when more than 10% of the works with
a known language are not in English; it reads language names such as
"German", ISO 639-1 codes such as "de" and ISO 639-2 codes such as
"ger". corpus$metadata$embedding records the model, its Hugging Face
id and snapshot revision, the text and prefix used, the versions of the
Python packages and the time (see ?sm_corpus). sm_certificate()
reports them in embedding_info, whose method now names the package or
service that made the embeddings, or is "unknown" for embeddings
without such a record (before: "recorded").
sm_embed_works() leaves works without text out of the embeddings, with
a warning giving their number: with the default text = "title_abstract"
the works with neither a title nor an abstract, with "title" or
"abstract" the works without that field (blank text counts as none).
Before, they were all embedded as the same empty text, so they became
each other's nearest neighbours, formed a cluster of their own and
stopped sm_plot_landscape(reducer = "tsne"). The row names of
corpus$embeddings are the embedded works, and the clustering functions
give the other works no cluster (NA), as they do for works missing from
a cache loaded with sm_embed_load().
sm_embed_works() works on a corpus with a single work; before, it
failed with "length of 'dimnames' [1] not equal to array extent".
sm_enrich_specter() fetches the SPECTER v2 vectors of Semantic Scholar
instead of the API's default SPECTER v1 and records them in
metadata$embedding. It fills gaps only in embeddings from the same
source: for embeddings of another model, or of a model that is not
recorded, it stops unless you set replace = TRUE (replace them by
SPECTER v2 vectors for every work with a DOI) or mix_models = TRUE
(keep them and add SPECTER v2 vectors for the other works, recorded as a
mix). Before, it silently mixed the vectors of two models when their
widths matched, and discarded every existing embedding, with only a
warning, when they did not. It now warns about failed requests also with
verbose = FALSE, tries a request once more after a short wait when
Semantic Scholar answers HTTP 429, and records its provenance rows as an
enrichment, which sm_rebuild_from_cert() does not replay.
sm_bind_corpora() and sm_build_corpus() gain mix_models. They stop
when the embeddings of two corpora record different models in
metadata$embedding, or when only one of them records its model, unless
mix_models = TRUE, which binds them and records a mix; embeddings of
corpora that record no model bind as before. sm_embed_save() keeps the record with the cache and
sm_embed_load() restores it, or leaves the model unrecorded for a cache
without one, and sm_fetch_semantic_scholar(include_embeddings = TRUE)
records its SPECTER v1 vectors.
Certificates record local input files with SHA-256
(sm_register_input(), sm_corpus_from_tables(inputs =));
sm_verify_certificate() checks them (base_dir), reports a status per
check and warns about inputs it could not find; sm_rebuild_from_cert()
names the local inputs it cannot replay. The checks of a verification
gain the columns status ("pass", "fail" or "not checked") and
location, and the result gains n_not_checked: a check that was not
made has pass = NA, and the overall result passes unless a check
failed. A relative input path is looked for in base_dir, in the folder
of the certificate file, in the working directory and last at the
absolute path recorded at registration. Printing a verification now shows
a symbol for each check, and printing a certificate the number of local
inputs. sm_bind_corpora() keeps the local inputs of both corpora.
sm_rebuild_from_cert() replays only the searches of OpenAlex, PubMed and
Crossref, through sm_fetch_openalex(), sm_fetch_pubmed() or
sm_fetch_crossref() with the engine, filter and n_max they were made
with. Before, it cut every search at 1,000 works and sent the file paths
of file reads and the placeholder queries of enrichments to the APIs as
search text. sm_fetch_openalex(), sm_fetch_pubmed(),
sm_fetch_crossref() and sm_corpus_for_question() now record kind,
filter and n_max in provenance, and sm_corpus_for_question()
records the client package that ran each search ("openalexR",
"rentrez" or "rcrossref") as its engine instead of "native", and
for Crossref an n_max of at most 1,000, the most its single request
returns; the file readers record the kind "file" and the file relative
to the working directory, or its name, instead of the path as given; the
enrichers record the kind "enrichment"; and certificates write file
steps without absolute paths. A message names the steps and local inputs
that were not replayed. When a certificate has no search to replay but
records file reads or local inputs, the rebuild stops with an error that
names them instead of returning an empty corpus; a certificate of only
synthetic records, enrichments, refreshes or searches of other sources
still gives an empty corpus. A search without a recorded n_max, as in
certificates of earlier versions, fetches up to the number of works of
the certified corpus. The rebuilt corpus holds the works of the searches
and their provenance only.
sm_hash_corpus() uses canonical text for hash version 2 (new argument
version), so its values no longer depend on the R version, platform,
time zone, collation locale, table column order or embedding row order.
Hashes of new certificates and snapshots differ from earlier ones for
the same content. Certificates and snapshots record hash_version;
those written without it use the unchanged earlier algorithm
(version = 1), whose binary serialisation depends on the R version.
Older certificates and snapshots remain verifiable on the R version
that wrote them. The hash leaves out provenance, which records fetch
dates, and metadata, including local inputs.
sm_cluster_label() ignores noise, scores terms by the share of a
cluster's works that use them (so one paper cannot name a cluster),
supports bigrams and non-English stop words, and gains
method = "keywords". The labels of the default method = "tfidf"
change: they hold single words and pairs of adjacent words (new argument
ngram, default 1:2), or the longer phrase that two overlapping terms
form (such as "exertional heat illness" for "exertional heat" and "heat
illness"), that occur in at least 3 of the cluster's works or 5% of
them, whichever is more (new argument min_works), count singular and
plural as one term, and leave out the stop words of each work's language
in works$language as well as the English ones, from the stopwords
package when it is installed; method = "yake" leaves out
these stop words too. The works of the noise cluster (new argument
noise, default 0L, as sm_cluster_hdbscan() assigns) get the label
NA from every method and no longer count for the labels of the other
clusters, so sm_plot_thematic_map(by = "cluster_label") leaves them
out; a cluster whose works share no term gets NA too, with a message.
The new column cluster_label_basis says how each label was made, for
example "tfidf (relaxed)" when no term reached min_works and the
terms occur in as few as 2 works, while cluster_label still holds only
the terms joined with "; ". method = "keywords" labels clusters by
their author and indexer keywords (such as Keywords Plus), and a cluster
without keywords by the terms of its titles; the new argument
stop_terms leaves out terms such as words every cluster shares.
Stability contracts, an enrichment materializer, and self-citation-corrected metrics, plus a verify-and-harden pass on three v0.3.0 fixes that recurred in heavy use. All new behaviour is opt-in; no breaking changes.
?scimapR-stability topic documents the package's accessor return-type
contract: a documented accessor's column set changes only via a lifecycle
deprecation and a NEWS.md entry. The audited accessors and their stable
shapes are listed there.sm_coverage_breakdowns() gains tidy = TRUE (default): a guaranteed long
tibble (dimension, level, n_reference, n_matched, recall,
n_corpus, precision, f1). tidy = FALSE returns the legacy recall-only
shape. (The silent list -> tibble change between 0.2.0 and 0.3.0 motivated
this contract.)sm_affiliation_signals(), sm_affiliation_methods(), and
sm_match_types() (with describe = TRUE for level + description). The
match_signal, match_method, and coverage match_type columns are now
factors with these exact levels, so downstream filtering cannot drift.These were reported again on a real corpus; each now has a regression test reproducing the recurrence.
sm_its(outcome = "cnci"): the resolver now also searches documented
impact side columns on works (fnci, rcr, ncs, mncs,
field_citation_ratio) and a corpus$metrics table, in addition to
works$cnci and works$cited_by_count. With no impact anywhere it errors
naming every column inspected. (v0.3 only checked works$cnci /
cited_by_count, so impact in a side column still produced "0 observations".)sm_count(level = "institution"): when structured IDs are absent it
now clusters raw_affiliation via sm_affiliation_match() (canonical names
where matched, raw string otherwise) with a warning, instead of counting raw
strings verbatim. Absent raw affiliation -> 0-row tibble + warning.precompute = TRUE confirmed; the max_nodes cap is now
opt-in (NULL default) so large renders are unchanged unless a cap is
requested. A ~2,000-node graph builds with precompute = TRUE and re-prints
without recomputing layout.sm_materialise(corpus, sources, .by = NULL, overwrite = FALSE) joins cached
enrichment (named list of tibbles / RDS / parquet paths, or a cache dir) into
the matching sub-tibbles by key, returning a validated sm_corpus. Missing
keys warn (not error); overlapping columns fill NA unless overwrite = TRUE..sm_bind_rows) that returns a
typed template instead of a logical-column degenerate when all parts are
NULL/empty; audited the package's existing bind_rows() sites (found safe,
guarded behind length checks with typed tibbles).sm_self_citation(corpus, level = c("author", "institution")) computes
self-citation from the corpus reference network (quota-light reference
overlap; no per-citation API calls), returning by_entity, by_work, and a
provenance tibble (citing_work_id, cited_work_id,
shared_author_id/shared_institution_id) (F1). Empty references ->
warning + typed empty result (no spin).sm_metric_h_index(), sm_metric_g_index(), sm_metric_m_index() gain
self_corrected = FALSE; with TRUE the index is recomputed after removing
self-citations (author/institution levels). The corrected index is always
<= the uncorrected one.sm_affiliation_summary() now includes example_evidence (a representative
matched-evidence string per institution x signal) alongside the factorised
match_signal, so a reader can see which signal matched and on what evidence.inst/extdata/example_self_citation_corpus.rds: a small references-bearing
synthetic corpus for self-citation examples/tests.Real-use refinements from running v0.2.0 on a ~6,853-work corpus: robustness fixes, friendlier API shapes, two new capabilities, and vignettes that ship in the installed binary. The public API is preserved (deprecation paths only).
sm_its(outcome = "cnci") no longer fails with a cryptic "Too few
yearly observations: 0" when impact lives outside cited_by_count. The
resolver now searches, in order, works$cnci, a corpus$metrics table, then
works$cited_by_count (deriving FNCI); if none is populated it raises an
informative error naming the columns it inspected.sm_count(level = "institution") falls back to
authorships$raw_affiliation (with a cli warning that results are
un-disambiguated) when structured institution IDs are absent, and warns
rather than returning a silent empty result when no institution data exists.sm_metric_disruption() and sm_metric_novelty() fast-exit with a
warning when the reference network is empty/absent instead of spinning; the
disruption index was rewritten with O(1) adjacency lookups (no longer
O(n^2)) and now shows a cli progress bar. sm_audit_summary() shows a
progress bar across its sub-audits.sm_plot_citation_network(), sm_plot_collab()) gain
precompute = TRUE (eager layout -> a self-contained plain ggplot that
prints cheaply in knitr/callr/workflowr subprocesses) and a max_nodes
cap (default 200) for very large graphs.sm_coverage_audit()$breakdowns is now a single flat tibble
(dimension, level, n_reference, n_matched, recall). The previous
nested list remains available under $breakdowns_nested for one release.
New accessor sm_coverage_breakdowns() returns/filters the flat tibble.sm_affiliation_match() documents its added columns and gains
sm_affiliation_summary() — a tidy works/authorships breakdown by
institution and match signal, also surfaced as a cli summary on completion.sm_corpus_from_tables() is promoted in the ingestion vignette as the
recommended "bring your own relational data" entry point.sm_coverage_audit(..., index_table = ) additionally assesses journal
indexability (reusing sm_journal_in_index()), adding issn/indexable
columns to $matches and an $indexability summary. Output is unchanged when
index_table is NULL.sm_affiliation_match() returns match_signal
(name_token/email_domain/postcode) and match_evidence (the matched
substring/domain/code) for an audit trail, plus an opt-in
postcode_signal = TRUE matcher (off by default so existing matches are
stable).vignette(package = "scimapR") / browseVignettes("scimapR"), or online at
the pkgdown site. New inst/extdata/ fixtures: example_affiliation_postcode.csv.A new capability layer for research-coverage, affiliation-attribution, and
policy-evaluation analyses, plus four reproducible bug fixes. All existing
functionality, the sm_corpus schema, bibliometrix interop, and the Shiny app
are preserved.
sm_coverage_audit() computes recall, precision, and F1 of a corpus against
a ground-truth reference (manual tracker, ORCID set, repository export, or
another sm_corpus), with per-year/source/affiliation breakdowns and
full match provenance. Returns an sm_coverage object with print,
summary, and autoplot methods.sm_journal_in_index() verifies source coverage against a user-supplied
journal master list by normalised ISSN (print and electronic), fully offline.sm_reconcile() performs a content-based symmetric diff (DOI then fuzzy
title) returning an sm_reconciliation (in_both/only_a/only_b +
provenance), with print/summary/autoplot. sm_diff_corpora() is now
marked superseded in favour of it (and continues to work unchanged).sm_affiliation_match() tags authorships with institutions using a
multilingual / synonym-aware dictionary and an email-domain fallback, handling
multiple affiliations per author.sm_attribute_institution() rolls matches up to a controlled vocabulary
(ROR-backed via an offline ROR table, or a custom vocabulary).sm_affiliation_dict: a default, documented, user-overridable dictionary.sm_its(): turnkey interrupted time series (level + slope terms,
counterfactual, autoplot), with automatic exclusion of citation-immature
years for citation-based outcomes.sm_did(): difference-in-differences for treated vs control institution sets.sm_synth(): synthetic-control helper (optional tidysynth, graceful error
when absent).sm_citation_maturity() flags citation-immature recent years
(citation_mature / cnci_provisional), wired into sm_its().sm_count(): full vs fractional counting at institution / author / source
level (output credit and fractionally-weighted impact).sm_metric_summary(robust = TRUE) reports medians with bootstrap CIs and
%PP(top-10%) alongside means (base-R resampling by default; boot optional;
reproducible via seed).sm_figure_manifest() scans a figure directory into a captions / alt-text /
dimensions manifest (optional magick/png, sidecar caption files, CSV/YAML
output).sm_corpus_from_tables(): a documented, validating constructor from a
relational set of data frames — the recommended ingestion path for arbitrary
tabular sources.sm_fetch_openalex(engine = "native"): abstract reconstruction from the
OpenAlex inverted index is now type-stable and never aborts a fetch on an
empty/NULL/malformed record (and no longer passes an invalid .default to
purrr::map_chr()).sm_fetch_openalex(engine = "openalexR"): long DOI (or other ID) lists
are auto-batched under the API's OR-filter limit via the new batch_size
argument, then row-bound and de-duplicated.sm_read_bib(engine = "bibliometrix"): a field-sparse @article no
longer triggers "undefined columns selected"; the scimapR wrapper catches the
failure and falls back to the native parser (clean-room rule preserved).substr()/grepl() scanning and per-entry tibble construction
to linear character-vector scanning with single-pass column assembly. Parsing
~7,000 entries dropped from ~100 s to ~17 s on R 4.5.2 / Windows.inst/extdata/: example_sparse.bib, example_openalex_inverted.json,
example_journal_index.csv, example_ror.csv.scimapR is a comprehensive R toolkit for bibliometric and scientometric
analysis: the reproducible, equity-aware, question-driven, AI-assisted
toolkit for working biomedical researchers. Designed as a complement to
the foundational bibliometrix package (Aria & Cuccurullo, 2017) with
first-class round-trip interop.
sm_refresh(), sm_staleness(), sm_lock().sm_question(),
sm_corpus_for_question(), sm_screen_against_question() with optional
LLM grounding.sm_certificate(),
sm_rebuild_from_cert(), sm_verify_certificate().sm_author_trajectory() with topic pivots,
collaborator turnover, emerging-collaborator detection.sm_audit_geographic(),
sm_audit_gender(), sm_audit_funding(), sm_audit_oa() with built-in
confidence reporting and limitation caveats.sm_chat() with retrieval-constrained
citations.sm_corpus) with provenance and screening tables