コンテンツにスキップ

安定したノート ID で Anki デッキを更新する

すでに配布した Anki デッキに訂正や新しいカードを届けます。ノートの ID を安定させると、Anki Forge と Anki は既存のノートを新規ノートとして扱わずに識別できます。ただし、実際に何が更新されるかには、Anki のインポート設定や、それより新しいローカルでの編集も影響します。

プロジェクトの名前空間と、ノート・モデル・フィールド・テンプレートのキーは変更しないでください。新しいリリースは、完全なバージョン付き識別情報を含む、前回の配布時のオリジナル APKGを基準に作成します。そのパッケージを対応するソースのリビジョンと一緒に保管してください。

ターミナルウィンドウ
cargo run --locked -p ankiforge --example docs_workflow -- target/docs-examples

このコマンドは spanish-v1.apkg と spanish-v2.apkg を書き出し、変更した回答を比較して、更新版のパッケージ内の件数を確認します。実行されるソースコードは次のとおりです。

fn vocabulary(answer: &str) -> anyhow::Result<Project> {
let mut project = Project::new("docs-spanish-updates")?.default_deck("Spanish");
project.add("hola", Note::basic("hola", answer))?;
Ok(project)
}
fn updates(output: &Path) -> anyhow::Result<()> {
let previous = output.join("spanish-v1.apkg");
let next = output.join("spanish-v2.apkg");
vocabulary("hello")?.build(BuildOptions::to(&previous))?;
let project = vocabulary("hello; hi")?;
let comparison = project.compare(CompareOptions::against(&previous))?;
ensure!(comparison.policy().allows_publication());
let built = project.build(BuildOptions::to(&next).update_from(&previous))?;
verify(&built, 1, 1, 0)?;
ensure!(built.report().comparison().is_some());
Ok(())
}

compare は、公開を妨げる検出事項があっても正常に完了します。元のリスクレベルとその根拠に加えて、comparison.policy().allows_publication() を別途確認してください。build(...update_from(...)) も同じ分析を行い、ポリシーが候補の公開を認めない場合は、公開前に BuildError を返します。

次の完全なプログラムでは、より厳しいしきい値を選び、比較と更新に同じ設定を使います。

use ankiforge::{BuildOptions, Note, Project};
use ankiforge::build::InspectLimits;
use ankiforge::update::{CompareOptions, RiskLevel, UpdatePolicy};
fn main() -> anyhow::Result<()> {
let mut first = Project::new("policy-example")?;
first.add("cell", Note::basic("Cell?", "Unit of life"))?;
first.build(BuildOptions::to("policy-v1.apkg"))?;
let mut next = Project::new("policy-example")?;
next.add("cell", Note::basic("Cell?", "The basic unit of life"))?;
let mut limits = InspectLimits::default();
limits.max_collection_bytes = 1 << 30;
let policy = UpdatePolicy::default().fail_on(RiskLevel::Medium);
let comparison = next.compare(CompareOptions::against("policy-v1.apkg")
.inspect_limits(limits.clone()).update_policy(policy.clone()))?;
assert!(comparison.policy().allows_publication());
next.build(BuildOptions::to("policy-v2.apkg").update_from("policy-v1.apkg")
.inspect_limits(limits).update_policy(policy))?;
Ok(())
}

デフォルトのポリシーは High と Critical の検出事項がある場合に公開を止めます。特定のカテゴリを確認したうえで、呼び出し側が意図的に UpdatePolicy::default().allow(RiskCode::NoteRemoved) を選ぶこともできます。この指定は、元のレベルと根拠を保持したまま、そのカテゴリの検出事項をすべて許容します。特定のノートだけを個別に承認する指定ではありません。該当する検出事項のない許容指定には警告が出ます。不明なリスクコードは拒否されます。比較で返されたコードをすべて自動的に許容しないでください。

識別情報の欠落や破損、名前空間の不一致、不正な候補データ、検査上限への抵触はエラーであり、許容指定できるリスクカテゴリではありません。分析を完了できない場合、比較はそれまでに得られた検査結果を含むエラーを返します。update_from のない新規作成に明示的なポリシーを指定すると、setter の呼び出し順にかかわらず設定エラーになります。

パッケージには ankiforge-identity-v1 形式の ankiforge-identity.json が含まれます。モデル/設定の ID、ノートの GUID、過去のフィールド/テンプレートのスロット、カード/マスクの通し番号、リビジョン、メディアを、実際のコレクションと格納データに結び付けます。Anki からの再エクスポートではこの情報が保持されないため、更新の基準には使えません。欠落した情報が暗黙に再構築されることもありません。

表示名や内容を編集しても、識別用のキーは保持されます。内容が変わらない場合は基準パッケージのリビジョンを維持し、変更した場合はリビジョンを進めます。フィールドやテンプレートを並べ替えても、過去の対応関係は保持されます。使われなくなった識別子も予約されたままなので、同じキーを復元すると再利用できます。ノートキーを別のモデルへ移すことはできず、モデルキーの種類を通常型と Cloze 型の間で変更することもできません。

クライアントのインポート動作と公開前の確認

「クライアントのインポート動作と公開前の確認」のセクション

APKG は配布用パッケージであり、削除を同期する仕組みではありません。ノート、フィールド、テンプレート、マスクを省いても、学習者の既存データは削除されません。フィールド/テンプレートのスキーマ変更と並べ替えフィールドの変更は、Anki のマージやインポートの設定が動作に影響するため、高リスクです。スキーマ変更によって、ノート内容の比較より先にモデルが複製されたり、対象ノートのタイムスタンプが更新されたりする場合があります。

サポートするクライアントで、初回インポートと既存コレクションへの更新をテストしてください。予定しているマージ設定とノート更新設定を使い、フィールド、GUID、カードの集合、ローカルでの編集、既存の学習スケジュールを確認します。識別情報が安定しているだけでは、新しいローカル編集の上書きや、削除された履歴の復元は保証されません。

候補は、保存済みの基準パッケージとは別のパスに書き出してください。確認を終えたら承認したパッケージを配布し、次回のリリースの根拠として保管します。ファイルの所有権と公開時の保証についてはビルドの保証を参照してください。

一度だけビルドし、確認してから公開する

「一度だけビルドし、確認してから公開する」のセクション

この API は公開パッケージ 0.3.1 に含まれています。

一度ビルドしたらレポートを確認し、その同じ候補ファイルを公開します。確認後に通常のビルドをもう一度行うと、別の候補が生成されます。prepare_publication で候補を準備し、確認後に publish で保存すると、レポートで確認したものと同じ APKG を書き出せます。

use ankiforge::{BuildOptions, Note, Project};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut project = Project::new("spanish")?.default_deck("Spanish");
project.add("es:hola", Note::basic("hola", "hello"))?;
project.build(BuildOptions::to("spanish-original.apkg"))?;
let mut next = Project::new("spanish")?.default_deck("Spanish");
next.add("es:hola", Note::basic("hola", "hello; hi"))?;
let prepared = next.prepare_publication(
BuildOptions::to("spanish-reviewed.apkg").update_from("spanish-original.apkg"),
)?;
println!("{:?}", prepared.report().comparison());
if prepared
.report()
.comparison()
.is_some_and(|report| report.policy().allows_publication())
{
let output = prepared.publish()?;
println!("{}", output.artifact().path().display());
}
Ok(())
}

固定したソースのチェックアウトで、上の完全なプログラムを anki_forge/examples/docs_publication.rs に保存します。この例は本ガイドで追加するもので、1199196 には含まれていません。その後、次を実行します。

ターミナルウィンドウ
cargo run --locked -p ankiforge --example docs_publication

この例はリポジトリのルートに spanish-original.apkg を書き出し、ポリシーが公開を認めた場合は spanish-reviewed.apkg も書き出します。準備の段階では公開先は変更されません。配布を承認する前に検出事項を確認し、クライアントへのインポートをテストしてください。この例が自動的に公開するのは、ポリシーが許容する単純な内容変更だけです。ベンチマークはこのワークフローを測定していません。

ビルドのライフサイクル、Rust API、Node API、Python APIも参照してください。

Markdown を表示