Skip to content

Public API overview

anki_forge is a typed Rust library for building Anki decks. The crate ships its default contract resources, so normal use does not require a source checkout, a particular working directory, or a separate runtime installation.

use anki_forge::prelude::*;
fn main() -> anyhow::Result<()> {
let mut deck = Deck::new("Spanish");
deck.basic()
.note("hola", "hello")
.stable_id("es:hola")
.add()?;
deck.write_apkg("spanish.apkg")?.ensure_success()?;
Ok(())
}

The crate version is 0.1.0; it embeds contract bundle 0.6.3. These are independent compatibility axes. Rust 1.92.0 is the minimum supported compiler for the 0.1.x line.

The supported consumer interface is intentionally small: import anki_forge::prelude::*, or use the root Deck, Project, and Severity exports. facade_api_version() and embedded_contract_version() expose the two compatibility axes. Normal consumers do not need contract loading, normalization IR, writer, inspection, or persistence modules.

The internal-tools Cargo feature exists only for this repository’s unpublished contract tool and deep conformance tests. Its hidden modules are not covered by the 0.1 compatibility promise and must not be enabled by downstream applications.

For a project with stable project/note identities, build updates using BuildOptions::new().output("v2.apkg").compare_to("v1.apkg"), where v1.apkg is the latest distributed version. Alternatively, use a maintained identity lockfile with .update_safe("identity.json").write_identity_lockfile(true) after the first .first_update_safe_build("identity.json") build. Keep the baseline separate from the new output and advance it only after verifying the release.

Changed note content advances its baseline modification time; unchanged content preserves it. This also covers answer-only edits, tags, and content reverts while keeping same-input/same-baseline builds reproducible. Legacy lockfiles without revision evidence require a previous APKG for strict migration. Baseline-free write_apkg is a first-release export, not a guarantee that Anki will update existing notes. Newer local edits remain governed by Anki’s import settings.

Report-only builds with missing or unreadable baseline evidence leave the identity lockfile unchanged, even if writing was requested, and report UPDATE.LOCKFILE_WRITE_SKIPPED_UNVERIFIED. Recover the evidence before retrying; rejected requested lockfiles are high risk and can be blocked with .fail_on(RiskLevel::High).

Register each source with deck.media().add(MediaSource::from_file(path))?, or project.media_mut().add_file(path)?.export_as(filename)?. Registration validates the source immediately; export checks for subsequent content changes. No batch registration method, thread setup or performance option is needed. File-backed Deck registration reads larger blocks and reuses image header bytes for dimensions.

Default temporary exports prepare media in parallel internally, stream through bounded encoding queues and retain final package inspection. Explicit artifact/CAS directories retain inspectable media files. These storage modes can produce different ZIP entry ordering and package hashes while preserving the same decoded content and Anki identities.

Project::from(deck) imports the Deck into editable Project state. You can then add notes, custom note types, template bundles, and media normally. Imported HTML, note identities, media sources, and diagnostic locations are preserved.

File-media input copies created during a build are owned only until normalization finishes and are cleaned up on failure too. Repeated builds and newly registered media updates can reuse an artifact directory. Existing caller-owned files and source aliases are never taken over or deleted.

build(BuildOptions::new()) returns a temporary ApkgArtifact. Keep the report or clone its artifact handle while using artifact.path(). The last report, error, or artifact clone removes the temporary APKG; staging files are already cleaned up when the build returns. Copying only the path does not keep it alive.

For a permanent file, use write_apkg(path), BuildOptions::output(path), or artifact.persist_to(path)?. The latter atomically copies the APKG and returns a persistent handle without changing the lifetime of existing clones. Explicit output and artifacts_dir destinations are never deleted by handle cleanup. Persisting a temporary artifact onto its own path is rejected.

Automatic report_json requires an explicit output or artifacts_dir, since JSON cannot own a temporary file. Manually serialized reports are snapshots; persist the artifact first if its path must remain usable after the handle drops. A late lockfile/report failure retains any already-published artifact in the returned error report.

APKG inspection has finite archive, entry-count, expansion, and zstd-window budgets. BuildOptions::inspect_limits(InspectLimits) applies the same policy to current and baseline APKGs. Start with InspectLimits::default() and explicitly raise individual fields only for trusted large decks. Resource failures carry INSPECT.RESOURCE_LIMIT_EXCEEDED; report-only baselines remain unavailable, and current-artifact failures prevent publication. These are decompression budgets, not a total process memory or CPU sandbox.

High-level Deck and Project operations return structured errors and build reports; callers should inspect stable diagnostic codes instead of matching human-readable messages. File-writing operations are synchronous and may leave diagnostic evidence in a requested report path when a build fails.

compare_to(...) baselines are read-only: output, report, and writable lockfile paths must not alias them, including through symlinks or hard links. Baselines, outputs, retained packages, and identity lockfiles must also stay outside writable staging/media directories, including directory aliases. Comparison and risk checks use a snapshot captured before building and run before APKG or lockfile publication. A policy-blocked build preserves existing outputs and lockfiles and reports diff/risk evidence with no artifact path. Publication is atomic per file, not transactional across all requested files. New destinations are rechecked after creation, so a late path-collision error may leave a valid published APKG but cannot replace it with lockfile/report JSON. Private candidates follow the artifact workspace’s filesystem and are cleaned up after building. Lockfile publication uses an exclusively reserved temporary file beside its target, without reusing predictable names that may alias inputs or outputs.

Values are ordinary owned Rust values and may be moved between threads when their fields permit it. A single builder or project is not designed for concurrent mutation; coordinate shared mutation in the calling application. The embedded read-only contract runtime is initialized once and can be loaded concurrently.

See the repository documentation for custom note types, media, update-safe builds, compatibility policy, and release operations.

Licensed under MIT.