codecheck: Export the register to Wikidata
Daniel Nüst
2026-09-04
Source:vignettes/codecheck_wikidata.Rmd
codecheck_wikidata.RmdThe register is also published as linked data: one item per certificate, per checked work, per codechecker and per venue, so that a CODECHECK can be queried and cited like any other scholarly object. This document is the procedure for doing that, start to finish. It is written to be followed literally, by a person or by an AI assistant, and the order of the steps matters - the last two write to Wikidata, where a mistake is public and has to be cleaned up by hand.
See codecheckers/register#50 for the modelling decisions behind all of this.
The two targets
There are two, and they behave differently.
| CODECHECK Wikibase | Wikidata | |
|---|---|---|
| Where | https://codecheck.wikibase.cloud | https://www.wikidata.org |
| Written by | this package, through the API | a person, pasting QuickStatements |
| Holds | everything: certificates, works, people, venues | certificates and the works they check |
| Repeatable | yes, entities are matched and updated | no, CREATE always creates |
| Disposable | yes, rebuild it from empty at any time | no |
The Wikibase is a staging instance, and the one rule about it is that
nothing may live only there. Everything on it is
generated from register.csv and the model in
R/wikidata.R, so it can be rebuilt from an empty wiki
whenever needed. Do not record anything there that exists nowhere
else.
Wikidata is the opposite: it is permanent, it is edited under a person’s own account, and QuickStatements has no undo. That is why the Wikibase load is the rehearsal and comes first.
Before you start
Install the package and have a rendered register available - the
export reads docs/register.json and
docs/certs/*/index.json from the register repository, which
is offline and takes seconds, rather than re-resolving everything from
the network.
Writing to the Wikibase needs a bot password from https://codecheck.wikibase.cloud/wiki/Special:BotPasswords with the “High-volume editing”, “Edit existing pages” and “Create, edit, and move pages” grants, in two environment variables. The last of these matters because the bootstrap creates the hosting-policy pages, and creating a page is a different grant from editing one:
The register’s .env is the place for them; its
Makefile exports them to the R process. Reading the
Wikibase, and all of Wikidata, is anonymous and needs no
credentials.
Two options are worth setting for any real run:
# an edit log: the only record of what was written, and the only record at all
# of the batches pasted into Wikidata by hand
options(codecheck.wikibase_log = "wikibase-log.csv")
# Wikimedia's User-Agent policy asks for a way to reach whoever runs the client
options(codecheck.contact = "you@example.org")Every step below is a dry run first. The dry run is not a formality - it is where a missing property or a wrong label shows up, and it costs nothing.
Step 1: bootstrap the Wikibase
The instance mints its own property and item numbers, so
P31 there is not Wikidata’s P31. This creates
one local property or item per entry of the model, each carrying a
“Wikidata entity” statement naming its counterpart.
bootstrap_wikibase() # what would be created
bootstrap_wikibase(dry_run = FALSE) # create itIt is idempotent: what already exists is left alone, matched by its Wikidata id rather than by label, so a partially failed run can simply be repeated. It also brings a label that has drifted from the model back in line, and writes the index at Project:Data model. Every run also writes Project:About and Project:Copyrights, which the wikibase.cloud hosting policy requires of every hosted instance, so that an instance rebuilt from empty comes back compliant.
Run this again whenever the model gains a property or an item - the load in the next step refuses to run against an instance that is missing any of them.
Step 2: load the register into the Wikibase
load_wikibase_register("../register") # what would happen
load_wikibase_register("../register", limit = 3) # a small rehearsal
load_wikibase_register("../register", dry_run = FALSE) # write itEntities are written in dependency order - people, venues, works,
then the certificates that refer to them - because a certificate’s
author and review of statements can only name
items that already exist. Each entity is matched by the identifier the
model resolves it on (report DOI, paper DOI, ORCID, ISSN), so a rerun
updates what it wrote last time instead of duplicating it. A second run
of a complete load therefore reports every entity as an update and
creates nothing.
The dry run returns the payloads it would send, which is the thing to look at before writing:
plan <- load_wikibase_register("../register")
payloads <- attr(plan, "payloads")
str(payloads[["certificate:10.5281/ZENODO.3674056"]], max.level = 2)Afterwards the instance holds the register, and Project:Certificates lists every certificate item next to its register page.
Look at the result before going on. Open a few certificate items and check that the statements say what they should. This is the rehearsal; everything after this point is public.
Step 3: preview the Wikidata export
preview <- preview_wikidata_export("../register",
out_dir = "wikidata",
publish = TRUE)This writes nothing to Wikidata. It resolves every certificate and
checked work against Wikidata, works out what exists and what would be
created, writes the QuickStatements batches to wikidata/,
and (with publish = TRUE) puts the whole preview on the
Wikibase as Project:Wikidata
export so it can be reviewed by somebody who does not run R.
Read the summary it prints. It tells you how many works are already
on Wikidata, how many would be created, and how many certificates cannot
yet state review of.
How existing items are found
By default resolution asks the Action API’s
haswbstatement search, which indexes a new item within
minutes and sees every item regardless of which graph it ended up in.
The query service is available as a cross-check:
preview_wikidata_export("../register", method = "sparql")Expect the two to disagree slightly, and expect the search to find more. The query service is hours behind, and each of its two endpoints serves only its own graph - a checked work typed as a report rather than a scholarly article is invisible to the scholarly endpoint even though the item exists. Resolving by SPARQL alone would create it a second time.
Why there are two batches
QuickStatements v1 can only refer to an item it has just created, as
LAST. A certificate therefore cannot name
a work created in the same batch. So the works batch runs first, and the
certificates batch is generated again afterwards, once the works have
QIDs to point at.
Doing it in the other order produces certificates with no link to the work they review, which is the one statement the whole model exists for.
Step 4: paste the works batch
- Open https://quickstatements.toolforge.org/ and log in with your own Wikidata account.
- New batch → V1 syntax.
- Paste the contents of
wikidata/wikidata-works.qs. - Import, and check the parsed preview QuickStatements shows.
Now QuickStatements offers two ways to run, and they differ in what you can watch and in what URL you end up with. Either is fine; the point is to know which one you used, because the record you keep in step 5 comes from a different place in each case.
Run (recommended) runs the batch in your browser,
reporting each command as it goes. You see a failure - a malformed
value, a blocked account - while there is still a run to stop, which is
why it is the better choice for a batch that has never been run before.
It needs the tab left open: closing it stops the run part-way. There is
no QuickStatements batch page. Instead the run shows a You can
revert or discuss this batch link to EditGroups, e.g.
https://editgroups.toolforge.org/b/QSv2T/1788464327978,
whose trailing number is the run’s start time. That is the URL
to keep.
Run in background hands the batch to the server. It
survives the tab closing, which is the reason to prefer it for a long
batch, but you only learn about failures afterwards. It gives a
QuickStatements batch page,
https://quickstatements.toolforge.org/#/batch/12345, listed
with your other runs at
https://quickstatements.toolforge.org/#/batches/<your username>.
That is the URL to keep.
Either way, watch the first few commands through before leaving it, and stop rather than letting a failing batch continue.
Wikidata’s edit rate limit
A normal account may make 90 edits per minute, and QuickStatements writes one edit per item. A background run pushes as fast as the API accepts, so a batch creating more than 90 items stops dead at the limit: everything from item 91 on is rejected, and QuickStatements reports it as
No success flag set in API result
which says nothing about the cause. The items already created are
fine - the run simply stops, and the batch page shows the split
(90 DONE, 42 ERROR).
preview_wikidata_export() therefore writes any batch
above the limit as several numbered files:
! 132 items is more than the 80 an account may create per minute: split into 2
batches to paste in turn
ℹ 1204 commands written to 'wikidata/wikidata-certificates-01.qs'
ℹ 781 commands written to 'wikidata/wikidata-certificates-02.qs'
Paste them in order, leaving a minute between them,
and record each one separately - each is its own batch with its own URL.
The default is a little under 90 so that other edits in the same minute
do not push the batch over; chunk_size changes it.
A split never falls inside an item: the statements after a
CREATE address it as LAST, so a boundary in
the wrong place would attach them to the previous item.
If it happens anyway - an older batch file, a hand-made one - the
recovery is step 6: regenerate, and the preview will offer exactly the
items that are still missing. That is the one case where
force = TRUE is right, because the guard cannot tell a
failed batch from a slow index on its own.
Both runs are also grouped on EditGroups, which is what makes a whole batch revertable in one go if it turns out to have been wrong - worth knowing about before running several hundred creates that have no undo of their own.
Step 5: record that the batch ran
quickstatements_submitted("wikidata-works",
url = "https://editgroups.toolforge.org/b/QSv2T/1788464327978",
note = "foreground run, 3 commands failed")Whichever URL step 4 left you with goes in url. If you
somehow have neither, record the batch anyway - url may be
NA - and say so in note; a run with no link is
still better known than unknown.
Nothing else records this. The code cannot observe a paste, and a batch run last week against a register that has since moved on cannot be reconstructed from anywhere else.
Recording it also arms the guard in step 6: a batch known to have been submitted, whose entities do not resolve yet, is the one case where generating the batch again would be a mistake.
And it retires the file: wikidata-works.qs is renamed to
wikidata-works.qs.submitted, so nothing in the directory
still looks pasteable when it has already been run. The commands stay
readable under the new name, which is what you want when you need to see
which of them failed. Once every work resolves, a later preview also
removes a leftover batch file for the same reason.
Step 6: regenerate, and check the gate
preview <- preview_wikidata_export("../register", out_dir = "wikidata")The gate: it must now report that the works are already on Wikidata and that there are none to create.
If it still reports works to create, the search index has not caught up yet - usually minutes. Wait and run it again.
You will not silently get a duplicate batch here: when the log says a batch of this name was submitted and the works still do not resolve, the file is not written and the reason is printed instead.
✖ Not writing the work batch: one was submitted at 2026-09-02T17:04:11+0200,
and 91 works still do not resolve.
ℹ Either the search index has not caught up - wait and run this again - or that
batch failed, which its QuickStatements page will say.
Find out which of the two it was - the batch page says whether it ran - before doing anything else. Only then:
preview_wikidata_export("../register", out_dir = "wikidata", force = TRUE)Do not paste the works batch a second time without checking.
CREATEhas no idempotency and nothing on the Wikidata side will stop you: pasting it twice creates a second item for every work, and those have to be merged by hand afterwards. To check one DOI directly:https://www.wikidata.org/w/index.php?search=haswbstatement:P356=10.1093/GIGASCIENCE/GIAA026
Step 7: paste the certificates batch
Same as step 4, with wikidata/wikidata-certificates.qs,
and then:
quickstatements_submitted("wikidata-certificates", url = "...")Step 8: verify
verify_wikidata_export("../register")This is the step that proves the model’s central decision worked rather than only sounding right. It asks two different questions of two different sources: the search index says whether each certificate item exists at all, and the query service says whether it is reachable in the scholarly graph, where the works it reviews are.
ℹ 132 of 132 certificates exist on Wikidata
ℹ 132 visible in the scholarly graph
ℹ 124 state review of on the work they checked
The returned table has a row per certificate, so a shortfall can be looked at directly:
result <- verify_wikidata_export("../register")
subset(result, !in_scholarly_graph | is.na(review_of))Run it a few hours after the batch, not immediately: the query service updater lags, and the scholarly endpoint is the slower of the two. An item that exists but is not yet visible is a lag; the same result the next day is a fault.
A certificate that exists but is not in the scholarly
graph is missing its P13046 statement. An item
lands in the scholarly graph if its direct P31 is one of
the scholarly types, or if it carries any P13046 - a
certificate is typed Q116740091 CODECHECK, which is not a
scholarly type, so P13046 is the only thing putting it
there, and without it the certificate cannot be joined to the work it
reviews.
A certificate that states no review of
either checks a work with no DOI (8 of them), or was created before the
works batch ran.
The edit log
With options(codecheck.wikibase_log = ...) set, every
write appends a row: time, target, action, kind, id, label, status,
batch, detail. It covers both halves of the export - the API writes to
the Wikibase, and the QuickStatements batches prepared for and submitted
to Wikidata.
log <- codecheck:::wikibase_log_read("wikibase-log.csv")
subset(log, target == "wikidata")If something goes wrong
The load stops with “The instance is missing N properties of
the model”. The model gained something the instance does not
have. Run bootstrap_wikibase(dry_run = FALSE) and load
again.
A write fails with maxlag,
ratelimited or a 503. It is retried automatically
with a growing pause. If it fails anyway, run the same command again -
both the bootstrap and the load are idempotent, and they pick up where
they stopped.
The export stops with “Two or more entities share an
identifier”. Two certificates name the same report DOI, or two
works the same paper DOI. That identifier is what finds an entity again
on the next run, so the export would write one item for both and the
second would overwrite the first - on Wikidata, irrecoverably. The
message names the rows; fix the register (register_check()
warns about this too) or drop the affected rows before exporting.
A statement is missing from an item. Almost always the value could not be resolved: a work without a DOI, a codechecker without an ORCID, a venue without an ISSN, or an item the model names that does not exist on the instance yet. Compare against the dry run’s payload for that entity, which shows exactly which statements were built.
Duplicate items on Wikidata. Merge them with the Merge gadget,
keeping the lower QID, and record what happened with
quickstatements_submitted()’s note. Then find
out which step was run twice before running anything again.
The Wikibase is in a bad state. It is disposable. Delete what is there and run steps 1 and 2 again; the register is the authority and nothing is lost.
What is deliberately not exported
- People and venues are never created on Wikidata. They are resolved against items the communities that maintain them own, and mirrored on the Wikibase instead.
- A work with no DOI gets no item, on either target: it could not be found again on the next run, so it would be created afresh every time.
-
A checked work’s publication comes from the work, not from
the register.
P1433published in is resolved by the ISSN in the work’s own OpenAlex record, not from the register’sVenuecolumn - one register venue can span several publications over the years, and a conference is not a publication at all. A work whose record names no ISSN (a preprint, a report) gets no such statement. The Wikibase additionally holds an item for every publication the works name, sincevenues.csvcovers only the venues that commission checks; on Wikidata those publications are resolved, never created. - Statements on items that already exist are not overwritten. A certificate or work Wikidata already holds keeps the label, description and statements somebody else chose.