Every glossapi command (plan, run, status, report, capabilities, doctor, install, guide) prints
exactly one JSON envelope. The schemas (JSON Schema draft 2020-12) ship with the package in glossapi/schemas/v1/:
| file | describes |
|---|---|
envelope.json |
the envelope: schema_version (1), command, corpus_id, run_id, data, warnings, errors |
error.json |
one typed error: code, message, document_id, stage, details |
common.json |
shared objects: capability, device, package status, conflict, warning, task counts |
plan.json |
plan --json data, including the exact preview (below) |
run.json |
run --json data |
status.json |
status --json data (sources with --file) |
report.json |
report --json data (sources = each document's original metadata, verbatim) |
report_why.json |
report --why SELECTOR data |
report_export.json |
the receipt of report --output |
capabilities.json |
capabilities --json data |
doctor.json |
doctor --json data |
manifest.json |
the input manifest v1 (--manifest) |
install.json |
install --json data (dry run or executed) |
guide.json |
guide --json data (the LLM guide) |
The runtime never validates against the schemas (jsonschema is a test-group dependency only); the tests validate
real outputs: the CLI test corpora (pytest -p cli_schema_validation ... validates every envelope those tests print),
a real extract → clean → export run, capabilities, doctor and every typed error.
- Within v1, only additive optional fields are allowed: a new key that consumers may ignore. Consumers must ignore keys they do not know.
- Anything else is v2: removing or renaming a field, making a field required, changing a type or an enum's
meaning, or changing an exit code's meaning. v2 gets a new
schemas/v2/directory andschema_version: 2. - Every change to a v1 file needs a changelog entry: append it to
schemas/v1/SCHEMA_LOCK.json(entry id, date, the change, and the new SHA-256 of every file) and list the same entry id below.tests/test_k3_interface.pyfails when a schema file's bytes differ from the latest lock entry.
0 success; 1 the command ran and found failures (status/run with failed documents, doctor with an
error check). A run that exits 1 keeps its data and lists the failures in errors: one per failed task (code =
the ledger reason code, e.g. clean_process_failed, with document_id, stage, the error message and
details.error_type), one source_unavailable per unavailable source, and otherwise one summary error
(run_completed_with_failures or run_cancelled). A status that exits 1 (a listed task failed) also keeps its
data and adds one error per failed task it lists (its --stage and --file filters apply), each exactly as run
reports it, or one run_completed_with_failures summary when the task table names none; errors is never empty
then. 2 a typed error (errors[0].code); 3 a policy or disk conflict (disk_budget_required,
disk_budget_exceeded, policy_conflict, waiting_for_disk, disk_full, disk_guard_unavailable), a run refused
because a corpus copy the last run left unflushed was edited after it closed (post_close_edit, the edited paths in
details.paths; move or delete them), a run refused because recovery found a corpus file it must repair but could not
repair safely (unresolved_recovery: hard-linked, contradictory lock records, path replaced, managed source changed or
missing; details.paths and details.conditions name each path and reason, the file is kept; remove the cause and
resume), or a run waiting for disk.
plan executes nothing and writes nothing (it reads the ledger read-only and hashes the selected new local sources).
Its data adds to the hashed plan body:
targets: each document to process, with path, state (present/absent/unreadable/remote), bytes, SHA-256, stages, and the download request for a remote source;exclusions: each requested entry that will not be processed, withreason_code(not_selectedfor an unselected manifest entry,duplicate_sourcefor a repeated source);capabilities: one capability object per planned stage, the missing installs and their install commands, and thefingerprintof the installed capabilities the plan depends on;device_allocation: the device, workers and CPU tokens per stage, whether the device was chosen explicitly, and the device's state (available,unknown_until_probe,unavailable);deletion_policy: whatdebug/memory_savingkeep, release or delete, after which planned readers, per document;conflicts: every conflict with its severity.errorconflicts failplanitself (exit 2, the preview stays indata):device_unsupported_for_stage,device_unavailable(an explicitly chosen device this machine does not have),device_option_conflict.blockingconflicts (missing_capability,empty_selection) keepplanat exit 0 withexecution.ready: falseand a warning, and makerunrefuse.memory_saving=extremeneeds positivedisk_budget_bytes,max_source_bytesandmax_attempt_bytes(disk_budget_required, exit 3 otherwise) and dedicated size-limited corpus and staging filesystems (disk_guard_unavailable, exit 3, on an ordinary shared volume); the exact requirements are in Resumability, recovery and retention.
Downloads are public targets only. A source URL with userinfo (user:pass@) or a credential query/fragment field
(token, key, signature, X-Amz-Signature, ...), or a download setting that would send credentials
(domain_cookies, credentials), fails plan and run with credentials_unsupported (exit 2) before anything is
planned, recorded or fetched. Every other URL is recorded as given.
An omitted selection (no --files, no --manifest) plans every corpus document. A manifest is an explicit selection:
one that selects nothing ("selection": [], or an empty pre-v1 list) plans zero targets, lists every entry in
exclusions, and carries the blocking empty_selection conflict; run refuses it with empty_selection (exit 2)
before the ledger is touched, with --manifest and with a saved --plan alike.
Every present local target is hashed at planning, including a source the ledger already knows: targets[].sha256 is
the content observed now (documents[].source_observation.sha256); the ledger's previous hash is kept separately as
source_observation.stored_sha256. An edited, previously processed file therefore plans and runs as a new version.
run --resume RUN_ID restores that run's frozen plan; any planning argument next to it is refused
(invalid_request). Interrupted or lost attempts (a killed worker, a timeout) are retried automatically, at most 3
attempts per task, and every interrupted attempt counts. Past that budget, and for any other failure, the task stays
failed; run --resume RUN_ID --retry-failed is the explicit, operator-authorised retry of the failed tasks of that run.
When the retry brings documents to ok, the corpus products are published again to cover them (a publication is
current only for the document versions it recorded); a committed product that was already released makes the resume
refuse with republish_inputs_released before it writes.
run --plan FILE is bound to the plan identity. Before the ledger is touched it refuses with a typed error when
the file is not a plan (invalid_plan), the plan body was edited (plan_hash_mismatch, for example a changed setting), a planned input changed size,
modification time or bytes, or appeared/disappeared (plan_source_changed), the installed capabilities changed
(plan_capability_changed), or a conflict blocks it (empty_selection, missing_capability, device codes).
capabilities lists every stage with the packages it needs (present, missing, version mismatch, not admitted),
the required native modules, the visible devices and the install command for each missing capability. Detection
uses importlib.metadata and importlib.util.find_spec only: no torch, docling, pandas or OCR backend is imported.
CUDA and MPS are unknown_until_probe unless the platform rules them out. A stage is available under the scheduler's
own rule (the OCR stage checks versions, the toolkit source and its entry point).
Each native_modules row of a required extension that carries a native API level (the cleaner and the noise
module) also has api_level (found in the built file), required_api_level (what this glossapi needs) and detail.
The level is read from the extension file's bytes, without importing it. A build from other source (no marker, or
another level) is shown as status: "missing" with a detail that starts stale native module and ends
rebuild core; clean is then unavailable with reason_code: missing_native_module and the install command
python install_glossapi.py core. These keys are additive optional fields of schema v1.
doctor adds environment checks, each with a fix hint: Python version, the Rust extensions (imported in a child
process), standalone native distributions that shadow the root wheel, free disk and writability at the corpus, and the
ledger. doctor --probe-devices imports torch in a child process to confirm CUDA/MPS and says so in device_probe
and a warning.
--catalog adds data.catalog (common.json#/$defs/catalog) to the capabilities data. It lists every install profile
(core and every pyproject extra): its purpose, the profiles it includes, its exact requirement lines, the stage
variants it enables (derived from each stage adapter's packages()), whether it is device-bound (needs a torch build),
the device profiles it supports per measured platform, the licences of its own requirements and of everything it adds,
its measured install size, and whether it is installed (installed.state: installed / partial / missing, per
requirement).
Proprietary licences are flagged explicitly (schema 1.2): licence.proprietary (boolean) and
licence.proprietary_packages per profile, over every target and device profile, and proprietary_packages per
measured lock. A package is flagged when its measured licence names a proprietary licence, or when it is an NVIDIA
CUDA runtime distribution (nvidia-*, cuda-toolkit) that declares no licence (licence.proprietary_basis). The
CUDA device profiles of layout/docling/ocr are flagged (the NVIDIA CUDA runtimes); their CPU and MPS locks list
none. The guide's package rows carry the same flag.
Why a flag on capabilities and not a separate packages command: the catalog is the same capability surface (the
same requirement detection and the same envelope command), so an LLM learns one read-only command for "what is here and
what could be added", the envelope dispatch stays unchanged, and the plan's install_commands point at the same names.
Sizes are measured, not estimated: tools/catalog/measure_profiles.py compiles one PEP 751 lock per profile, device
profile and platform (uv pip compile, CPython 3.12, linux-x86_64 manylinux_2_28 and macos-arm64 14.0) from exactly the
requirements install would request, and records per lock the package count, the download bytes (the lock's wheel
sizes) and the installed bytes (uncompressed sizes from each wheel's ZIP central directory, read with range requests),
plus the packages added over the core lock. Packages without a wheel (the Greek OCR toolkit is a VCS source) are listed
in unmeasured. Each lock records the hash of its inputs; when the requirements change, measurement.state becomes
stale, capabilities --catalog warns catalog_measurement_stale, and the tests fail until the tool is re-run.
- The default is a dry run:
data.commandsholds the exact commands (installer, index URLs, requirement lines),data.requirementsthe installed state of each line, anddata.would_changewhat would change (the missing or mismatched requirements, and an estimate from the measured locks of the packages added and their bytes). Nothing runs.would_change.from_lockis labelled an estimate: the exact answer needs the network (the installer resolves for real and may differ). --yesruns exactlydata.commandsin order and stops at the first failure, then re-runs the capability check in a fresh interpreter:data.executed(each step's exit code, time and output tail, and the installer's dependency check) anddata.changes(distributions added/removed/changed, stages whose availability changed, capability fingerprints). While a command runs, its output is copied to stderr line by line;output_tailkeeps its last 40 lines. stdout stays one JSON envelope.- Requirement lines are shown with their version specifiers in one canonical order (exact and lower bounds, then upper
bounds, then
!=), so a wheel install and a source checkout show the same lines, commands and lock-input hash. - Only what was requested: the commands carry the requested profiles' own requirement lines, with
glossapi[...]self references expanded (browser includes download), and nothing else. glossapi itself is never reinstalled (the source installerinstall_glossapi.pydoes that). A device-bound profile (layout, ocr) first installs the device's torch/torchvision build from its index; the second command pins the same build. - One device profile per environment:
cpu,cuda126,cuda132(Linux) ormps(Apple Silicon). Without--packagenothing is installed (warningnothing_selected). --installer auto|pip|uv: auto uses pip when this interpreter has it, elseuv pip install --python.
The versioned LLM guide (guide_version 1): the phases and the two products, the choices (files, devices, packages,
debug, memory_saving) with their measured costs, the workflow (capabilities, choose, plan, run, retry failed tasks
after the user agrees, status/report, report --why), how to hand over files found elsewhere (input manifest v1; glossAPI does not scrape), the typed errors
and the schema files. docs/llm_guide.md is the same content rendered by python -m glossapi.llm_guide. Every fact
comes from the module that owns it; tests/test_k3b_interface.py fails when the guide, the Markdown file, the schemas,
the settings contract or the catalog disagree.
guide and capabilities --catalog are read-only and exit 0; their only errors are the generic ones (invalid_request
for a bad argument, read_error). install checks the whole request before anything runs; every refusal below leaves
the environment untouched, except install_command_failed, which reports what ran.
| code | exit | when |
|---|---|---|
device_required |
2 | no --device |
conflicting_device_profiles |
2 | two device profiles at once (--device cpu --device cuda126, cpu,mps) |
unknown_device |
2 | not one of cpu, cuda126, cuda132, mps |
device_unsupported_on_platform |
2 | mps off Apple Silicon macOS, cuda126/cuda132 off Linux |
unknown_package |
2 | a package not in the catalog (details.known lists them) |
package_device_unsupported |
2 | the package's lock for that device profile did not resolve, or no measured platform has it |
installed_device_profile_conflict |
2 | the environment holds a torch build of another device profile |
requirements_unavailable |
2 | glossapi is neither a source checkout nor an installed distribution |
installer_unavailable |
2 | --yes without pip in this interpreter or uv on PATH (the dry run warns instead) |
install_command_failed |
2 | a command exited non-zero; data keeps the steps, output tails and the changes so far |
invalid_request |
2 | a malformed argument (for example --installer conda) |
Warnings: nothing_selected, installer_unavailable (dry run), dependency_check_failed (after --yes),
catalog_measurement_stale (capabilities --catalog).
{"manifest_version": 1,
"documents": [{"path": "/abs/a.pdf", "selection_id": "a", "metadata": {"title": "…", "any": ["JSON"]}},
{"path": "/abs/b.pdf", "selection_id": "b"}],
"selection": ["a"],
"stages": ["extract", "clean"], "products": ["document_export"],
"devices": {"ocr": "mps"}, "workers": {"clean": 2},
"settings": {"debug": "off", "memory_saving": "normal"}}- Each entry names exactly one source:
path(absolute),url(unchanged URL behaviour), or a ledger reference (document_id,source_key,source_uri).metadataand any other entry fields are the caller's original metadata;status --fileandreportreturn them verbatim (sources[].metadata.manifest_row). - A remote entry may give
source_idandurl_index(a nonnegative integer). Plan and run key a declared source by (dataset_id, source_id, url_index); a list of URLs in one entry is keyed by each URL's position. Two different URLs with the same identity are refused by plan and run withinvalid_manifest(details.reason: ambiguous_source_identity,details.index,details.urls); an invalidurl_index(negative, fractional, a string or a boolean) withinvalid_manifest(details.field: url_index). The same URL twice stays one target (the repeat is aduplicate_sourceexclusion). selection(optional) names theselection_ids to process. Other entries appear inexclusionsand are never opened, hashed or processed. An emptyselectionselects nothing (empty_selection, seeplan); omit it to process every entry.products:document_exportaddsexport;annotated_sectionsaddsannotate.- Settings precedence: manifest
settings<--settings FILE< individual flags. Stages, devices and workers given both in the manifest and as arguments must agree (conflicting_request). - A manifest without
manifest_version(a list or{"documents": [...]}) keeps the pre-v1 behaviour, except that an empty one selects nothing (empty_selection) instead of every corpus document. Other versions fail withunsupported_manifest_version; malformed entries withinvalid_manifestanddetails.field/index. - Python:
glossapi.input_manifest.build_manifest,load_manifest,write_manifest;glossapi.cli.plan_documentsreturns the same plan data asglossapi plan --json.write_manifest(likereport --output) resolves its destination first and refuses a hard-linked file (MetadataAliasError) or one that resolves to corpus metadata parquet through a symlink or..(MetadataDestinationError) before writing anything.
1.0(2026-10-31): initial freeze: envelope, error, plan, run, status, report, report --why, report --output, capabilities, doctor, input manifest v1.1.1(2026-10-31): additive (K3b): new commandsinstallandguide(install.json,guide.json, envelope dispatch for them);capabilities.catalog(with--catalog) and the common definitionscatalog,package_profile,lock_size,requirement_state,device_profile. No existing field changed.1.2(2026-10-31): additive (K3F round 2, K3B-M2): an optional proprietary-licence flag in the catalog (package_profile.licence.proprietary,proprietary_packages,proprietary_basis;lock_size.proprietary_packages) and in the guide package rows (proprietary,proprietary_packages). No existing field changed.