The app and its API¶
cartolex.app is the web app: the API the screens use and the server of the
interface’s files. The same code serves one person on their own computer and a
hosted service. The engine packages and the packages below the app (the
project format, the build) never import it (tests/test_layering.py).
from cartolex.app import AppSettings, create_app
app = create_app(AppSettings(project=folder)) # an ASGI app, e.g. for uvicorn
cartolex # the app on a loopback port, in the browser (the last project)
cartolex app my-project # the same, with this project open
cartolex api my-project --host 0.0.0.0 --allowed-host maps.example.org # hosting
cartolex api --projects-root /srv/projects --allowed-host maps.example.org
The OpenAPI description of every route is at GET /api/openapi.json.
The modules¶
module |
what it holds |
|---|---|
|
|
|
|
|
|
|
|
|
the manifest the interface starts from (The app manifest, cartolex-manifest/1) |
|
the project context of each request; local and hosted project hosts; paging |
|
sessions, the launch link, the host check, CSRF, headers; the guard on every route; |
|
|
|
the error shape; versions as |
|
the |
|
the site builder protocol; receiving files; serving the interface |
|
JSON log lines; running the server |
|
one module per screen |
Per request: the project context¶
A route that works on a project takes the dependency ProjectDep
(cartolex.app.deps): a ProjectContext with the project (open for
writing), its lock and its paths. Locally one project is open at a time:
POST /api/projects/open, /close, GET /api/projects/recent change and
list it, and the app holds its lock until it closes it or stops. Hosted,
many projects live in one folder (--projects-root); a request names its
project by the route, /api/projects/<id>/<the local path without /api>
(/api/projects/coast/keywords), or its principal carries one (a host’s
sign-in). Each is opened on first use. Without an open project, a project
route answers 409 no_project with the next action open-project.
Security¶
The launch link. Each app has a random launch token;
cartolex appopens the browser at/launch?token=…and prints the link. The first visit exchanges it for a session (anHttpOnly,SameSite=Strictcookie) and goes to/; the link does not work a second time. A hosted service can sign people in its own way (AppSettings.authenticate), which opens a session the same way.The host check. A request whose
Hostis not a name the app answers to is refused (400) before anything else: loopback names locally, the--allowed-hostnames when hosted.CSRF. Every
POST,PUT,PATCHandDELETEsends the session’s token inX-Cartolex-CSRF; the interface reads it from the cookie the manifest names. A request with a cross-siteOrigin(ornull) is refused.No CORS. No
Access-Control-Allow-*header is ever sent.Headers. Every response carries
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; … object-src 'none'; base-uri 'none'; frame-ancestors 'none'(no inline script, noeval),X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer, andX-Request-ID..jsfiles aretext/javascripton every platform (a fixed table, never the system’s).Inputs. Bodies are pydantic models with bounded sizes; a request larger than 16 MB (uploads: 50 MB; a copilot’s result: 256 MB) is refused before it is read. Uploads are written only inside their folder under a clean name; an archive is checked member by member (no absolute path, no
.., no link, no hidden name, at most so many members and bytes once unpacked) and refused whole.Authorisation. Every route calls
authorize(principal, action, resource)through its guard (cartolex.app.routing.Guard; a test fails when a route has none). Locally there is one trusted principal, the person who opened the link. A refusal is 403 with a plain message; no session is 401 with the next actionsign-in.
Errors and empty results¶
Every error has the same shape (V2-016): a stable code, its params, a
message in plain English, and next, what to do: a label and an action
key the interface maps to a route or a command (reload, retry, confirm,
fix-input, open-project, sign-in, wait, build, settings,
report, none). The server does not translate: the interface
shows the text of the code from its catalogues (English, French, Portuguese),
filled with the params, and falls back on message. Every code is declared
once, with its English text, in cartolex.app.errors.ERRORS (listed below);
a test fails when a route raises a code that is not there or leaves out a
param its text names. Some errors add fields (current for a stale write,
job for a busy project).
{"error": {"code": "stale", "params": {"file": "themes.json"},
"message": "this changed elsewhere since it was read; reload it and apply the change again",
"next": {"label": "Reload", "action": "reload"}, "current": "sha256:…"}}
A list or a result with nothing in it says what to do next (V2-013), the same
way: "empty": {"code": "empty_no_keywords", "params": {}, "message": "no keywords yet: build the keywords first", "next": {"label": "Build the keywords", "action": "build"}}. The state of a stage carries its skip reason
(skip), its last failed attempt (attempt) and the reasons of an update
(reasons[]) with codes too (cartolex.app.messages.MESSAGES); the English
skip_reason and attempt.error stay beside them.
Versions: ETag and If-Match¶
Every read of a decision file answers with its version in ETag: the
fingerprint the guarded writer checks (sha256:…), or none for a file not
written yet. Every write sends the version it read in If-Match (without it:
428). When the file changed in between, the write is refused with 412, the
current version in ETag and in error.current, and nothing is written: the
interface reloads and merges. The atlas bundle has an ETag too, its lineage;
If-None-Match gives 304 when it did not change.
Lists¶
Lists are paged, sorted and filtered on the server: offset, limit (at most
500), sort (a field, -field for descending), q (a text filter) and each
list’s own filters. The answer: items, total, offset, limit, sort,
sorts (the fields it can sort by), filters, empty, and each list’s
counts.
Jobs¶
Builds, collections and site builds run as jobs, outside the request,
through a JobRunner (submit, status, progress, cancel, list, events).
LocalJobRunner runs each in a thread of the app; a queue can stand behind the
same interface. A build of cartolex’s stages runs in a process of its own
(cartolex.app.build_run): the child writes under the app’s project lock (as
its holder), sends its progress back and stops on a cancel or when the app is
gone, and its memory goes back to the computer when it ends; a child the
computer stops (most often for want of memory) fails the job with that reason.
A registry or an AI access given in code, or AppSettings(build_in_child=False),
keeps the build in the job’s thread. One job runs per project at a time: a second build or
collection is refused with 409 busy, naming the running job. A job’s states
are queued, running, cancelling, succeeded, waiting (a build that
ended at an AI step whose route is a copilot, waiting for its result: not a
failure), paused (a long job stopped with its state saved, to be resumed: not
a failure), failed, cancelled and interrupted. A cancel stops at the next safe point, and the result says
« nothing changed » or « finished before the cancel »; a job that keeps
checkpoints pauses instead.
A failed job says why. Its error is
{code, params, message, exception, detail, step, progress, traceback}: the cause’s
code (collect_budget_spent, collect_service_unavailable,
collect_incomplete, collect_malformed, collect_refused,
collect_cache_miss, else job_failed) with its params and English words,
the exception’s class and a short message (300 characters at most), the stage
it was in, its last progress, and the traceback (of the failed stage, or of the
build’s process; at most 8,000 characters, its end, with the home folder written
~; also in the job’s log). The interface’s error card shows the code’s words,
and « Copy a diagnostic » carries the job’s id, the time it failed, the system,
the class, the message, the step and the traceback.
A spent daily budget (collect_budget_spent, keyed among its params) names
its next action: without a key, a free one (open:/settings?section=sources);
with one, waiting for the next day’s budget (none).
A paused job’s result is {outcome: "paused", pause}; pause is
{code, params, message, checkpoint, progress, cause}: why it paused
(collect_size_confirm, collect_stopped, collect_paused, collect_budget_paused), how far it got,
the checkpoint to resume from, and the error that stopped it (shaped as a
failed job’s error), if any. The work of a job raises
cartolex.project.checkpoints.JobPaused after saving a Checkpoint; a new
job given the checkpoint goes on from it (for a collection,
POST /api/collection/start {action, resume: <checkpoint>, consent: true}).
The institutions’ proposal is the first job built this way; the people import’s
identification, the harvest, the text providers’ improvement and the
collaborators’ rounds are long collections that should use it next.
Every job writes logs/jobs/<job id>.jsonl in its project: a job line (its
kind, the process, a digest of the machine’s name and its boot), the build’s own events (phases,
stage ends with counts and times) and a job-end line (its state, and the
error of a failed job or the pause of a paused one) — never a name or a
text; a collection adds one egress line per host whatever its end, and a search of collaborators one phase line per phase as it ends (its seconds, requests sent and read from the cache, counts). After a restart, a job whose log has no end and whose process is gone is
interrupted, never running.
Logs and the diagnostic¶
The app logs JSON lines through logging (cartolex.app): each request with
its id, method, route template (/api/jobs/{job_id}, never the values or
the query), status and time. The server installs the formatter
(cartolex.app.logs.configure_logging); creating an app changes no logging
setting. GET /api/diagnostic gives the versions (Python, cartolex and its build, the key
libraries, the language models), the machine’s size and the recent job events
— never a project’s name, path, people or texts — so it can be pasted into a
report as it is.
The routes¶
The app
route |
what it does |
|---|---|
|
|
|
the manifest and its schema (The app manifest, cartolex-manifest/1) |
|
who the session acts for; sign out |
|
a page of the interface says it is open, or ( |
|
what the About page shows: |
|
versions and build, machine, recent job events |
|
this API’s description |
|
the launch link |
Projects: GET /api/projects/current, POST /api/projects/open {path, force}
(force: override a lock held elsewhere, after a warning),
POST /api/projects/close, GET /api/projects/recent (locally);
POST /api/projects/forget {path} takes a project out of the recent list (its folder
stays; answers the list); GET /api/projects/removal?path= says what deleting a recent
project’s folder would remove (path, name, bytes, files, kept: the entries
cartolex did not write, which stay with the folder, open: the project open here, held:
the application that holds it elsewhere) and POST /api/projects/delete {path, confirm}
deletes it (locally, cartolex.app.removal): the open project is closed first, then,
under the project’s lock, what cartolex writes at a project’s root (project.json last),
links removed as links and never followed, and the folder when nothing else is left in it
(deleted, folder_removed, kept, closed). Refused: a folder not in the recent list
(project_not_listed), not a cartolex project, a link, the home folder or one holding it
or the app’s own folder (project_delete_refused, reason: not_a_project, link,
missing, protected), without confirm (project_delete_confirm), while another
application holds the project (project_delete_held, never overridden) or a job runs on it
(busy), and on a hosted service (project_delete_hosted); a file the system refuses
stops it (project_delete_failed, the project still opens);
GET /api/projects (hosted: the projects the principal may open);
POST /api/projects {folder | id, name, domain_title, domain_description, languages, reference, display, start} creates a project, with the extensions’ slots,
projected sets and identity, and opens it; display defaults to the reference
and the corpus languages; start (people, institutions, collaborators,
folder, corpus) is where it starts from: a folder or a corpus adds a slot of
that kind, and the answer’s next is the page to go to.
GET /api/projects/defaults gives the suggested folder (~/cartolex-projects,
locally), the starting points and the languages; POST /api/projects/demo {folder} (locally) creates the demo project (the S demo world: an invented
community of coastal and marine sciences, in English and French) in an empty or
new folder (default ~/cartolex-projects/demo) and opens it; it still needs a
build.
State and building
route |
what it does |
|---|---|
|
every stage’s state (the six states, as keys: |
|
|
|
the route of each AI step, kept in |
|
the tracker: the running or last build job (its progress: phase, stage, fractions, ETA, message) and each of its stages, done, running or waiting, with counts and times, and |
|
what the overview adds to the state: |
|
jobs; a job’s |
Parameters and map versions
route |
what it does |
|---|---|
|
per stage, each parameter’s effective value, origin ( |
|
replace |
|
how people and organisations are compared ( |
|
the versions, newest first, and the pinned one |
`POST /api/map/versions {action: pin |
try |
|
the recipe of the build, read-only: |
|
the recipe as a download: Markdown (one table per stage, changed values marked |
|
the diagnostic of a step (shown in its page’s « Tune » panel), read from its stages’ outputs and cached by their runs ( |
|
what these thresholds would keep of the last build’s candidates and vocabulary, read from the stored candidates and nothing saved: |
|
a layout of a sample of the people (at most 800) with a method and its parameters, beside the map on the same people, each with the nearest people it keeps: 200 |
|
the base maps: other projects’ maps copied into this one ( |
|
copy another project’s map (locally; |
Snapshots: GET /api/snapshots (each decision file’s versions and each
stage’s current and previous generation), GET /api/snapshots?file=themes.json,
GET /api/snapshots/{file}/{version} (its content), and
POST /api/snapshots/{file}/{version}/restore (If-Match; a restore is a new
version, so it can be undone too).
People and collection
route |
what it does |
|---|---|
|
people with their role, set, identity state, records, unit, extra columns, coverage (texts, texts with an abstract, years, a class: good, thin, none) and coverage |
|
roles, sets, notes ( |
|
rows that are one person ( |
|
undo merges ( |
|
pairs of people who may be one person ( |
|
the review by groups ( |
|
two to 50 people side by side, from the project’s data only: each one as in |
|
people decided together ( |
|
two people side by side from the project’s data only: names and aliases, ORCID, records, identity, role, affiliations with their years, texts (count, years, the most recent titles), co-authors in the project; what they share (texts, organisations, co-authors); the pair’s evidence |
|
|
|
the automatic merge of the clear pairs, or with |
|
why a profile is what it is: the coverage and its first blocking cause, the sources used and discarded (each discarded one with |
|
a CSV file (a form’s |
|
add the people ( |
|
a form: |
|
organisations as people decided them ( |
|
one organisation: parents, units, each person’s affiliation with its years and source (those of the organisations merged into it too), |
|
rename an organisation, change its level or parents ( |
|
organisations that are one ( |
|
pairs that may be one organisation: the same ROR or OpenAlex id ( |
|
add or remove an affiliation, or |
|
texts with their providers, languages, richest content ( |
|
one text: each part by provider with a preview, its people, the records merged into it ( |
|
what an action would do and what leaves the computer: each host with its purpose, what it is sent, the requests and their cost at the service’s prices; what never leaves; what is kept and where; notes ( |
|
the collection job: |
|
the identity queue ( |
|
decide one identity, or accept the single clear match of many (the others are |
|
coverage per class and role; the four states and their first blocking causes; states by organisation; texts by year (with an abstract, titles only) and by language; the slots’ summary |
|
collaborators round by round with joint texts, fit and path, and the cap, rounds and cut of the latest run; decisions |
|
the latest search of institutions (each with its ROR, acronym, city, country and parents) and the latest proposal of their people (paged), with the levels and the suggested merges, each with its |
|
the latest proposal’s people, paged and searched ( |
|
a folder or corpus slot’s files; upload a document or a zip archive into |
Keywords, themes, atlas
route |
what it does |
|---|---|
|
the candidates of the current extraction in their bands ( |
|
the keywords of the last vocabulary build that someone’s row of the space holds (the whole lexicon the themes use), paged: |
|
the same, every row, as CSV ( |
|
the word cloud of the lexicon’s 200 most important keywords (by score or by people) as SVG: the |
|
the decisions of |
|
keep or exclude every keyword the list’s filters keep ( |
|
keep, exclude or merge, one or many ( |
|
undo decisions (an exclusion restored) |
|
the saved tree, or the draft of the last grouping, with the keywords the tree lacks or holds too many |
|
apply operations of |
|
save a new version ( |
|
versions |
|
a build job of the themes and the map |
|
what the map draws at any depth of the theme tree (levels, nodes, people, keywords, units, the number of time windows and their years, projected people, bounds), |
|
the people’s time windows as columns, |
|
the texts placed on the map (on built map version |
|
the keywords a region spans, by id (at most 500 ids): a person’s most used keywords (at most 40), or those of an organisation’s current members; every person’s keywords are read once per run of the keywords and of the corpus |
|
(not shown by the atlas, whose panel lists co-authors; for scripts and extensions) the |
|
who writes with a person (the people of the project who signed a work with them) or an organisation (the organisations of its level whose people signed works with its people), from the authorships ( |
|
two people or organisations side by side: |
|
what the Map screen’s Distances pane measures with ( |
|
who writes with whom over the bundle’s order, as the offline site carries it: |
|
the Distances pane’s measures that a browser cannot compute ( |
|
body |
|
the people who use a keyword (or a form merged into it), ranked by the share of their keyword use it holds ( |
Sharing, settings, the AI proposals
route |
what it does |
|---|---|
|
the site builds in |
|
the privacy summary of a build with these options (people named or pseudonymised, organisations, keywords, themes, texts carried, full texts kept out, |
|
build the offline site (a job, 202): |
|
a build’s files (to open it in the browser), and the build as one zip whose README comes first (written once, file by file, to |
|
the map as an image of the size asked, light or dark (people never named) |
|
the theme tree as CSV |
|
write the map bundle ( |
|
delete a site build and its zip ( |
|
delete every build but the latest, with their zips, and what interrupted work left (an unfinished build’s hidden folder, a zip whose build is gone): |
|
delete an exported file and the |
|
languages, language models, the AI identity (with what changing a frozen one costs: 409 |
|
the candidates rejected automatically: whether the project uses the rejection lists (the |
|
the cache’s terms (one per term and language: how many projects gave it, the routes, the last day), paged; remove some terms, or empty the cache (of one language); |
|
the curator’s notes for the AI copilot and the standing rules agreed with it ( |
|
the words added to and removed from the lists of words that are never keywords, per language ( |
|
the prompts a project may replace (the packaged text, the project’s own in |
|
a zip of |
|
a backup (multipart |
|
remove the built results (every stage is then never built); decisions, texts and caches stay; 409 |
|
this computer: the keys saved on it ( |
|
the folder of an OpenAlex snapshot downloaded to this computer, its full path ( |
|
what the builds the app starts may use of this computer ( |
|
the keyword proposals: a copilot’s triage results (The AI copilot), and the answers to a handoff (a prompt and a list pasted in a chat) an earlier version imported, still read and accepted ( |
|
the two routes of the AI filtering: with a copilot ( |
|
filter by API: switch |
The person: GET /api/me/preferences and PUT /api/me/preferences {locale, theme, dismissed_jobs, other}: the interface language, the theme, the
finished jobs dismissed from the Activity list (200 at most) and a few other
settings of the person signed in, kept in the app’s own folder, so they outlive
the browser’s storage (a hosted service: they follow a person from one browser
to another). The interface reads them at start and saves every change; the
browser keeps a copy to paint the theme before the first request.
The collection notices: GET /api/me/notices lists the kinds of
collection whose notice the person acknowledged (acknowledged: kind, at;
version, the notice’s version), and DELETE /api/me/notices forgets them
all: every notice is shown in full again. They are kept in the app’s own
folder, per person (<data dir>/users/<digest>.notices.json, never in a
project), in memory when the app has no folder.
The interface: /static/… (the interface’s files: .js as
text/javascript; charset=utf-8, .css as text/css, .json as
application/json, .svg as image/svg+xml), /static/ext/<id>/… (an
extension’s), and every other address that is not under /api or /static
answered with the shell (index.html): the interface routes in the browser.
A page whose module is missing is listed in the manifest with
/static/pages/placeholder.js, and a warning is logged once.
Collection behind a protocol¶
Importing people and collecting texts go through a CollectionService
(cartolex.app.collection): describe(), propose_import(),
confirm_import(), plan(action, options), collect(control, action, options)
(in a job) and candidates(). A list is imported by
cartolex.collect.people_import for every service (cartolex.app.importing
turns the interface’s one field per column into its mapping).
ServiceCollection(settings)(cartolex.app.collect_service, the default of the command line) runs the finders ofcartolex.collect:identify(resolve, HAL by name, SciELO with a collection),harvest(OpenAlex and ORCID, HAL by idHAL, missing abstracts on request),institutions(a search, or the people of the institutions chosen),collaborators(snowball) andretry(retry_failed). Each phase has its ownHttpClient, whose progress and cancel are the job’s; the job’s log gets oneegressline per host (the kinds of data sent, never the values). Its plan comes fromcartolex.collect.privacy.plan_collection, with HAL, SciELO and the text providers added, and every message carries a code for the catalogues.cartolex app --services demo --world S:0runs it against the demo services of a demo world, on this computer; the tests do the same.UnavailableCollection(the default ofAppSettings) imports lists but collects nothing.DemoCollection(world)answers from a demo world without leaving the computer or reaching a service (actioncollectonly).
The reading routes (coverage, organisations, texts, a person’s sheet,
collaborators, the institutions’ proposal) read the project’s tables and raw
records (cartolex.app.corpus_view), whatever the service; each list is
computed once per version of what it reads and kept in the app’s cache.
The site builder is a SiteBuilder protocol (cartolex.app.share):
builds, folder, build. The default is cartolex’s own
(cartolex.site.OfflineSiteBuilder, The offline site and the Share screen); StubSiteBuilder lists
earlier builds and refuses new ones, for a host that shares elsewhere.
Error codes¶
Every code the API answers with, its status, the English text (the fallback of
message, with {param} for each param) and its next action. The interface’s
catalogues give each code its text in every interface language.
code |
status |
English text |
params |
next |
|---|---|---|---|---|
|
400 |
this address is not one cartolex answers to; open it from the address the cartolex command gives |
— |
|
|
403 |
a change was asked from another site; cartolex takes changes from its own pages only |
— |
|
|
413 |
the request is larger than the limit ({limit_kb} KB) |
|
|
|
400 |
the request’s length is not a number |
— |
|
|
401 |
sign in first: open the app from the cartolex command (its launch link) |
— |
|
|
403 |
the change was refused: the {header} header is missing or wrong (reload the page) |
|
|
|
403 |
{reason} |
|
|
|
428 |
this change needs the version you read: send it in If-Match (the ETag of the read) |
— |
|
|
400 |
If-Match names one version |
— |
|
|
412 |
this changed elsewhere since it was read; reload it and apply the change again |
|
|
|
422 |
the request is not valid: {problems} |
|
|
|
404 |
no such address in this app |
— |
|
|
405 |
this address does not take this method |
— |
|
|
400 |
the request was refused ({status}) |
|
|
|
500 |
something went wrong inside cartolex ({error_type}) |
|
|
|
404 |
no such file in the interface |
— |
|
|
422 |
{locale} is not an interface language; choose among {locales} |
|
|
|
422 |
cannot sort by {sort}; sort by one of {sorts} |
|
|
|
409 |
no project is open: open one, or create one |
— |
|
|
400 |
name the project in the address: /api/projects/ |
— |
|
|
400 |
{id} is not a project id |
|
|
|
404 |
there is no project {id} |
|
|
|
404 |
a hosted app names its project in the address |
— |
|
|
404 |
{path} holds no cartolex project |
|
|
|
409 |
the project is in format {found}; this cartolex reads {expected} |
|
|
|
409 |
the project is open in {app} (process {pid} on {host}, since {since}); close it there, or open it anyway if that computer is off or the app is stuck |
|
|
|
409 |
the project is open in another {app} on this computer (process {pid}, since {since}); close that one (its browser tab does not stop it: close the terminal it runs in), or open it anyway if it is stuck |
|
|
|
409 |
this app no longer holds the project: {app} (process {pid} on {host}, since {since}) opened it anyway; nothing more is saved here, open the project again |
|
|
|
409 |
the folder already holds a project, or is not empty: {path} |
|
|
|
422 |
choose the folder of the new project |
— |
|
|
422 |
the project’s folder is a full path: {path} |
|
|
|
422 |
a hosted project needs an id |
— |
|
|
404 |
{path} is not among the projects listed here |
|
|
|
409 |
on a hosted service a project’s files are deleted by whoever runs it |
— |
|
|
409 |
{path} cannot be deleted from here ({reason}) |
|
|
|
422 |
deleting the folder {path} cannot be undone: confirm it to delete it |
|
|
|
409 |
the project is open in {app} (process {pid} on {host}, since {since}); close it there before deleting its folder |
|
|
|
409 |
the folder {path} was not deleted entirely ({error}); what is left can still be opened or deleted again |
|
|
|
422 |
name the field the map covers (its title): the AI receives it with the terms |
— |
|
|
422 |
cartolex has no language pack for {languages}; choose among {available} |
|
|
|
409 |
the project’s identity is frozen: changing its {changed} means cached AI answers are not reused (they are paid for again) or texts are parsed again; confirm the change to make it anyway |
|
|
|
422 |
a file of the project is not valid: {detail} |
|
|
|
422 |
nothing to change |
— |
|
|
409 |
a {kind} job ({job}) is already running on this project; wait for it or cancel it |
|
|
|
409 |
a job is running {stages}; wait for it or cancel it |
|
|
|
422 |
{item} is neither a stage nor an area; stages: {stages} |
|
|
|
422 |
the parameters were refused: {problems} |
|
|
|
404 |
there is no job {job} |
|
|
|
409 |
the job has already ended ({state}) |
|
|
|
409 |
this job runs in another process; stop it there |
— |
|
|
409 |
on a hosted service the keys are set by whoever runs it |
— |
|
|
409 |
on a hosted service there is no OpenAlex snapshot folder of this computer |
— |
|
|
422 |
{folder} cannot be the OpenAlex snapshot ({reason}): give the full path of a folder holding data/jsonl/works, authors and institutions |
|
|
|
409 |
on a hosted service the builds’ budget is set by whoever runs it |
— |
|
|
422 |
{field} cannot be {value}: {reason} |
|
|
|
409 |
on a hosted service there is no rejection cache of this computer |
— |
|
|
422 |
a word is both added and removed: {words} |
|
|
|
404 |
there is no prompt {name} to change |
|
|
|
422 |
the prompt cannot be read: {detail} |
|
|
|
422 |
the prompt uses placeholders the AI clean-up does not fill: {unknown} |
|
|
|
422 |
this file is not a backup of a cartolex project |
— |
|
|
404 |
there is no map version {version} |
|
|
|
422 |
name the version to {action} |
|
|
|
404 |
the map version {version} is not built: mark it to be built and build the map |
|
|
|
422 |
the {method} layout draws flat maps only: a map in space needs umap |
|
|
|
409 |
the pinned map of {name} ({version}) is in space, and a base is a flat map: pin a flat version there (or build one) to serve as a base |
|
|
|
409 |
a base map places a flat map only, and {version} is a map in space: show a flat version |
|
|
|
409 |
{version} is pinned: pin another version before discarding it |
|
|
|
409 |
there is no pinned map version to start from: build the map first |
— |
|
|
422 |
the {method} layout takes no parameter {param}; it takes: {known} |
|
|
|
422 |
the {method} layout needs the optional {package} package, which is not installed on this computer: run the installer again, or {command} |
|
|
|
409 |
this preview needs {stage} built first |
|
|
|
404 |
there is no base map {base} |
|
|
|
404 |
the {kind} {id} has no place in the space of this map |
|
|
|
422 |
{path} holds no project with a map: build its map there first |
|
|
|
422 |
a project’s own map cannot be its base: choose another project |
— |
|
|
409 |
a map version is placed on {base}: discard it before removing the base |
|
|
|
409 |
on a hosted service a base map is added by whoever runs it |
— |
|
|
404 |
{file} has no versions; files with versions: {files} |
|
|
|
404 |
{file} does not exist yet |
|
|
|
404 |
there is no version {version} of {file} |
|
|
|
409 |
version {version} of {file} cannot be rebuilt from the history: {reason} |
|
|
|
409 |
this version is already the current one |
— |
|
|
404 |
unknown person id(s): {ids} |
|
|
|
422 |
there is no projected set {set}; sets: {sets} |
|
|
|
422 |
a projected person belongs to a projected set: add one in the settings first |
— |
|
|
422 |
a person cannot be merged into themselves |
— |
|
|
409 |
{target} is itself merged into {into}: merge into that person |
|
|
|
409 |
{target} and {source} have different ORCIDs ({orcids}): two different iDs are two people; merge them anyway only if you know they are one person |
|
|
|
422 |
send the file in a form, as ‘file’ |
— |
|
|
422 |
send the list in a form (as ‘file’), or as {“text”: …} |
— |
|
|
422 |
the list holds nobody |
— |
|
|
404 |
this import is not waiting any more |
— |
|
|
422 |
unknown field(s) {fields}; the fields are {known} |
|
|
|
422 |
the list has no column(s) {columns} |
|
|
|
422 |
map a column to the last name, or to the full name |
— |
|
|
422 |
{role} is not a role |
|
|
|
409 |
collecting texts is not available in this version |
— |
|
|
409 |
the OpenAlex snapshot of this computer is not ready to be read ({state}): plug in its disk, finish its download, or read OpenAlex from the API |
|
|
|
409 |
the project has no slot to collect into: add one in the settings |
— |
|
|
409 |
no collection is running |
— |
|
|
404 |
there is no paused collection {checkpoint} to resume (it ended, or started again) |
|
|
|
404 |
there is no person {person} |
|
|
|
422 |
a record is scheme:id (orcid:0000-0002-1825-0097, openalex:A123…) or an ORCID iD |
— |
|
|
409 |
this record is not a candidate of this person; paste an id instead |
— |
|
|
409 |
none of these people has a single clear match; decide them one by one |
— |
|
|
422 |
{action} is not a collection action; actions: {actions} |
|
|
|
422 |
search institutions by a name, or choose the institutions to read |
— |
|
|
409 |
this collection sends data to {hosts}: read what leaves the computer, then confirm |
|
|
|
422 |
{n} keywords at once is more than {max}: narrow the filters |
|
|
|
409 |
the AI filtering by API needs a key (Settings, AI) and a provider in the project |
— |
|
|
409 |
the AI filtering by API sends keywords to {provider}: confirm first |
|
|
|
404 |
there is no organisation {org} |
|
|
|
404 |
there is no text {text} |
|
|
|
404 |
nobody was proposed from institutions yet: read institutions first |
— |
|
|
422 |
the import was refused: {detail} |
|
|
|
422 |
no document (PDF, text) in what was sent |
— |
|
|
422 |
the archive holds no index (a CSV file at its top) |
— |
|
|
404 |
the project has no slot {slot} |
|
|
|
409 |
slot {slot} is filled by collection, not by uploads |
|
|
|
413 |
the file is larger than the limit ({limit_mb} MB) |
|
|
|
422 |
the name {name} leaves its folder |
|
|
|
409 |
{name} is already there; nothing is replaced (rename the file to add it) |
|
|
|
422 |
the file is not a zip archive |
— |
|
|
413 |
the archive holds more than {max_members} files |
|
|
|
413 |
the archive unpacks to more than {limit_mb} MB |
|
|
|
422 |
the archive was refused: a member is not allowed ({problem}); nothing was written |
|
|
|
409 |
the archive would replace {name}; nothing was written |
|
|
|
422 |
a member of the archive is larger than it says |
— |
|
|
422 |
{language} is not a corpus language |
|
|
|
422 |
merge {term} into another keyword |
|
|
|
422 |
{term}: a {decision} takes a category among {allowed} |
|
|
|
404 |
none of these keywords has a decision |
— |
|
|
422 |
the tree is not valid: {detail} |
|
|
|
422 |
the change was refused: {detail} |
|
|
|
422 |
step {step} ({op}) was refused: {detail} |
|
|
|
409 |
build the keywords first |
— |
|
|
404 |
no term to send in this band |
— |
|
|
404 |
there is no proposal {proposal} |
|
|
|
422 |
choose the terms to accept |
— |
|
|
501 |
building the offline site is not available in this version |
— |
|
|
409 |
there is no map to share yet: build the map first |
— |
|
|
422 |
say whether the site shows people’s names or pseudonyms |
— |
|
|
404 |
there is no site build {build} |
|
|
|
404 |
there is no exported file {name} |
|
|
|
409 |
the build {build} is kept outside the project’s outputs; it is not deleted here |
|
|
|
422 |
say whether the file names people or gives them pseudonyms |
— |
|
|
409 |
this matrix has {cells} cells, about {size}: confirm to write it |
|
|
Message codes¶
Empty results (empty), skipped stages (skip), failed attempts (attempt), the
reasons of an update (reasons[]) and the collection plan carry code, params and
the English message the same way; an empty result also names its next action.
code |
English text |
params |
next |
|---|---|---|---|
|
no people yet: import a list of names |
— |
|
|
nothing matches these filters |
— |
|
|
no keywords yet: build the keywords first |
— |
|
|
no themes yet: build the themes to get a first draft |
— |
|
|
the tree was never saved |
— |
|
|
no keyword sits near the border between two nodes |
— |
|
|
the grouping did not read the texts: build the themes with the comb |
— |
|
|
every keyword sits on the level its texts support, or you kept it there |
— |
|
|
the texts are not gathered yet: build the corpus |
— |
|
|
the keywords are not placed in a space yet: build the themes |
— |
|
|
the keywords are not grouped yet: build the themes |
— |
|
|
no map yet: build the map |
— |
|
|
no map yet: the first build draws one and pins it |
— |
|
|
everything is up to date |
— |
|
|
nothing was built yet |
— |
|
|
no job has run yet |
— |
|
|
no project opened yet |
— |
|
|
{file} was never written |
|
|
|
no site built yet |
— |
|
|
no site built yet; building a site comes in a later version |
— |
|
|
no collection has run |
— |
|
|
nobody waits for a check |
— |
|
|
no two people look like one person |
— |
|
|
nobody is in this state |
— |
|
|
no term to send in this band |
— |
|
|
no AI answers imported yet |
— |
|
|
no term rejected by an AI on this computer yet |
— |
|
|
no decision yet: keep, exclude or merge keywords in the list |
— |
|
|
collecting texts from bibliographic services is not available in this version; import texts into a folder or corpus slot instead |
— |
— |
|
switched off |
|
— |
|
done with your copilot ({n} decisions, {date}) |
|
— |
|
waiting for your copilot: give it the candidates, then import and accept its result ({total} earlier decisions still apply) |
|
— |
|
the clean-up is at « No AI »: the candidates found since your copilot’s triage enter by their bands, and its {n} decisions still apply to the ones it judged; choose « With your copilot » or the API on the build page to have them judged |
|
— |
|
no AI clean-up: choose a copilot or the API on the build page, or use Triage with AI on the Lexicon screen, to have one |
— |
— |
|
the project has no overlay |
— |
— |
|
{reason} |
|
— |
|
the stage was cancelled; its previous results are kept |
— |
— |
|
the stage could not run: {detail} |
|
— |
|
no mapped person has texts yet: collect their texts first |
— |
|
|
nobody is mapped yet: on the People page, choose the people whose texts make the map |
— |
|
|
the AI clean-up by API needs an AI provider: choose one in the settings, or another route for the clean-up on the build page |
— |
|
|
no AI key is saved on this computer: add one in the settings, or choose another route for the clean-up on the build page |
— |
|
|
your themes could not be carried over to the new vocabulary: open the themes and save them again |
— |
|
|
no map version is chosen: choose one in the map’s settings |
— |
|
|
this computer cannot draw the map’s method ({method}): choose another method in the map’s settings |
|
|
|
a language model is missing: {detail} |
|
— |
|
the stage failed ({error_type}): {detail} |
|
— |
|
the job failed ({error_type}): {detail} |
|
— |
|
{host} refused more requests (status {status}): its daily budget is spent; set a free API key, or wait for the next day’s budget |
|
a free key without one ( |
|
{host} gave no usable answer after every attempt ({what}); try again later |
|
— |
|
{host} cut a page of a list short; collect again |
|
— |
|
{host} gave an answer that could not be read ({what}) |
|
— |
|
{host} refused a request (status {status}); copy a diagnostic and report it |
|
— |
|
an answer is not in the cache: collect without cache-only |
— |
— |
|
{total} works are signed there: reading them takes about {requests} requests and {seconds} s; confirm to go on, or narrow the years or the units, or read the OpenAlex snapshot instead |
|
— |
|
stopped after {works} of {total} works; resume to go on |
|
— |
|
a page still failed after its retries; {works} of {total} works are kept: resume to go on |
|
— |
|
the daily budget is spent; {works} of {total} works are kept: resume once it comes back ({resets_at}) |
|
— |
|
the map was drawn from inputs that changed since; building the map restores it |
— |
|
|
the language model {model} for {language} is not installed; the keyword extraction needs it |
|
|
|
{stage} needs about {need_mb} MB of memory and this machine has about {budget_mb} MB |
|
|
|
the texts are in {languages}: without the AI clean-up, keywords of each language may form themes of their own |
|
|
|
{share}% of the keywords are not in {language}: in a space of texts, themes may split by language; the people’s space may suit this corpus better |
|
|
|
the last proposal of collaborators in {slot} stopped at the cap of {cap} people |
|
|
|
a copilot’s result was imported and is not accepted yet: review it |
|
|
|
{n} mapped people without texts wait for an identity check |
|
|
|
{n} mapped people have no texts: their texts were never collected |
|
|
|
{n} mapped people have no texts: collect them first, or build without them |
|
|
|
a copilot’s result was imported and is not accepted yet: the build does not use it until you accept it |
|
|
|
with the clean-up at « No AI », the candidates are found again: your copilot’s {n} decisions still apply to the ones it judged, but new candidates enter the vocabulary by their bands, without its triage; choose the copilot below to have it judge them first |
|
— |
|
the vocabulary holds {keywords} keywords: the themes have {depth} level(s) instead of the {asked} asked |
|
— |
|
the build will now ask your copilot for new keywords: the keyword clean-up’s route is « With your copilot »; you can switch it back on the build page |
— |
|
|
the AI clean-up by API judged the candidates before; with this route its verdicts no longer decide which candidates the vocabulary keeps |
— |
— |
|
{terms} candidates: several conversations or agents may be needed; the kit cuts them into {parts} parts |
|
— |
|
{param} at {value} reaches past the last build’s {built}: the candidates outside its window were never kept, a new extraction shows them |
|
— |
|
a build is running |
— |
|
|
a {kind} job is running |
|
|
|
nobody is mapped yet: choose the people whose texts make the map |
— |
|
|
{n} people wait for an identity check before their texts can be collected |
|
|
|
{n} mapped people have no texts yet: collect their texts |
|
|
|
look at the keywords the build found: keep, set aside or merge them |
— |
|
|
draw the map |
— |
|
|
share the map: build a site you can send or publish |
— |
|
|
the build waits for your copilot ({step}) |
|
|
|
start with the people whose texts make the map |
— |
|
|
install the language model the keyword extraction needs |
— |
|
|
{stage} failed: see why and build again |
|
|
|
build the keywords, the themes and the map |
— |
|
|
the map is out of date: building it restores it |
— |
|
|
some results need an update |
— |
|
|
check the themes the grouping proposed and name them |
— |
|
|
everything is up to date |
— |
|
|
{detail} |
|
— |
|
{detail} |
|
— |
|
{detail} |
|
— |
|
{detail} |
|
— |
|
{detail} |
|
— |
The atlas and the theme editor¶
GET /api/atlas: cartolex-atlas/3¶
The atlas reads only the theme files of any depth (themes_applied.json,
theme_keywords.csv, theme_people.parquet, theme_organisations.parquet,
the levels of positions.json; see
Derived results, caches, outputs and logs), never the two-level files that exist at depth 2
only, so a project of depth 1, 3 or 4 gets its map like one of depth 2.
{
"format": "cartolex-atlas/3", "available": true, "lineage": {"map.layout": "…"},
"map_version": "v1", "depth": 2, "source": "decisions", "weights_basis": "tf",
"people_counted": 38,
"levels": [{"level": 1, "names": {"en": "Theme"}}, {"level": 2, "names": {"en": "Topic"}}],
"nodes": [{"id": "s1", "parent": null, "level": 1, "order": 1, "names": {"en": "…"},
"color": "#d22d3c", "weight": 3.6, "share": 0.097, "keywords": 2,
"keywords_counted": 42, "top_keywords": ["…"], "x": -2.3, "y": 3.6}],
"people": [{"person_id": "p0001", "name": "…", "unit": "…", "x": -3.4, "y": 4.0,
"shares": [{"s1": 0.25, "s8": 0.75}, {"c3": 0.25, "c9": 0.75}]}],
"keywords": [{"term": "tide gauge", "x": -4.3, "y": 3.3, "node": "c3", "level": 2,
"counts_to": 2, "weight": 0.07, "share": 0.002}],
"units": [{"unit": "…", "x": 0, "y": 0, "size": 7, "ellipse": {"sx": 0.5, "sy": 1.0, "rho": -0.1},
"shares": [{"s1": 0.5}, {"c3": 0.5}]}],
"windows": 52, "window_years": {"min": 2006, "max": 2026},
"overlays": [{"set": "applicants", "person_id": "p0041", "x": 0, "y": 0, "shares": [{}, {}]}],
"bounds": {"xmin": -6, "xmax": 6, "ymin": -5, "ymax": 7}
}
levelsandnodescome fromthemes_applied.jsonof the map (nodes in tree order, with their map position, the mean of their people’s).sharesis one{node id: share}per level, from the top: the usage share of the person (the organisation, the projected person) that counts toward each node of that level; each level sums to 1 where there is usage. A keyword’snodeisnullwhen it is set aside.windowscounts the people’s time windows placed on the map, andwindow_yearsgives their first and last years: a map of a hundred thousand people has millions, so they come apart, as columns, fromGET /api/atlas/windows(every one when they are shown, the selected person’s otherwise), each with its largest top-level node (trajectory_themes.parquet).The
ETagdepends on the format and the lineage;If-None-Matchgives 304.
The atlas page (/map) reads, beside the engine’s results, what the tables
say (cartolex.app.atlas_layers), so the ETag also follows the tables:
organisations: every organisation (id,name,acronym,level,parents,location{lat, lon}or null), placed (x,y) at the mean of the mapped people affiliated to it now, directly or through an organisation below it;memberscounts them,members_evercounts the people of the corpus ever affiliated to it (the same way, past affiliations included).organisation_levels: the project’s levels, smallest first, with their names and how many organisations each has.columns: each extra column of the people on the map with its values and counts (a column with one value, or more than 200, is left out);people_extra: person id →role,columns,orgs(current organisations);years: the first and last year of the texts;bases: the project’s base maps.?base=<id>answers the bundle placed on a base map: each keyword the base has at its place there; each person at the mean of their keywords’ places there, weighted by use; keywords the base lacks, people without a shared keyword, units, time windows and projected people are not placed;baseholds the base’s name, map version, the shared keywords’ count, its people’s places and its top-level themes, andboundsare the base’s.GET /api/atlas/textsplaces each text at the mean of the map’s keywords found in its title and abstract (runs of up to six words, keyword aliases included;by: 0), else at the mean of its authors on the map (by: 1);termsindexeskeywordsof the bundle. A corpus of more than 100,000 texts (total) is drawn by a sample of 100,000 (sampled; the map says so, and a person’s panel then leaves out their count of texts); only the sampled texts’ parts are read.
The theme tree¶
route |
what it does |
|---|---|
|
the saved tree ( |
|
the grouping’s latest proposal, whatever tree is saved: |
|
each keyword of the current vocabulary: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rebases the saved tree onto the current vocabulary now, as an apply does first (send |
|
|
A clustering-only change (the grouping ran again with other parameters, on
the same vocabulary) leaves the saved tree as it is. proposal.pending is true
until someone adopts the new proposal or keeps the tree over it: the tree
names the grouping it agreed with in based_on.run (a tree saved from a
proposal, adopted or kept), else the grouping the last apply read (after a
rebase).
Theme answers imported by an earlier version¶
The AI curation of the themes is the copilot’s (The AI copilot). The answers
to a theme handoff (a prompt and the tree as text, pasted in a chat;
cartolex-themes-handoff/1, cartolex.project.themes_handoff) that an earlier
version imported stay readable:
route |
what it does |
|---|---|
|
the theme results imported so far, newest first: the copilot’s ( |
|
an earlier answer read into proposed operations ( |
The editor shows a proposal as a list to accept or reject, previews the
accepted operations on the tree, and applies them through
POST /api/themes/ops.
The AI copilot¶
The copilot’s bundle is a zip an assistant able to run code works from on its
own; its result comes back as one file. Its format, the kit and the routes
(/api/themes/copilot/…, /api/keywords/copilot/…) are in The AI copilot.
An imported themes result is reviewed as a list of changes; a triage result is
a keyword proposal (GET /api/ai/proposals/{id} and its accept take
the ids of both).
Errors of the theme editor¶
code |
status |
message |
next |
|---|---|---|---|
|
404 |
the grouping has proposed no tree yet: build the themes first |
|
|
409 |
a newer proposal ({run}) replaced the one you saw: look at it first |
|
|
404 |
the tree holds no keyword to send |
— |
|
409 |
the keywords have no space yet: build the themes first |
|
|
422 |
this is not a copilot result of cartolex: {detail} |
|
Keeping over an unanswered proposal, and the versions’ names. When a
new grouping of the same vocabulary waits for an answer
(GET /api/themes → proposal.pending), POST /api/themes/apply first keeps
the saved tree over it, as a new version whose action is « keep the curated
tree over the proposal <run id> at an apply », and its answer adds
kept: {run, action, version}. GET /api/themes/versions gives each version
names and names_after: the names of the nodes its action names, in the
version before it and in the one it made (a node merged away is named by the
version that still had it).