Accepted (2026-08-25)
changes computeは、2つのref間で各Featureディレクトリの Git tree SHA を比較することで変更を検出している(src/changes.rs)。Knowledgeスキーマの移行によって全てのfeature.yml/behavior.yml等が機械的に書き換えられた場合、意味は変わっていなくても全FeatureのtreeSHAが変化するため、全FeatureにChangeEventが生成され、全ての生成テストケースがimpacted_testcasesに含まれてしまう。実際の内容変更と区別できない。
将来のcanonical model converter(本ADRの対象外、後述)が、任意のGit refについて「そのrefで有効だったKnowledgeスキーマバージョンは何か」を知る手段が必要になる。現状これを記録する仕組みは存在しない。.markharness/knowledge/*.ymlにバージョンマーカーはなく、.markharness/config.toml唯一のフィールドはマーカーファイル形式自体のschema_versionで、0010はこれを互換性チェックのために読み取ることを明示的に先送りしていた(「読み取り側の互換性チェックは実装しない(将来必要になった時点で追加する、YAGNI)」)。Issue #29は、この必要性が具体化したものである。
0013は既に本ADRが踏襲するパターンを確立していた: config.toml内の独立したテーブルに置かれた、狭いスコープを持つ2つ目のschema_version([identity].schema_version)であり、マーカーファイル自体のトップレベルschema_versionとは独立している。リポジトリには他にも複数の独立したschema_version相当の値が既に存在する — バージョン付きJSON出力envelope(src/presentation.rsの"schema_version":1)、id解決/identity registryキャッシュキーのCANONICALIZATION_RULE_VERSION/ID_INDEX_SCHEMA_VERSION(src/id_cache.rs、src/identity/registry.rs)。これらは互いに共有されない。本ADRはKnowledgeコンテンツ自体のためのスコープを持つ、もう一つのバージョンを追加する。
config.toml専用の[knowledge].schema_versionschema_version = 1
[knowledge]
schema_version = 1
トップレベルのマーカーファイルschema_version、[identity].schema_version、JSON出力envelopeのschema_version、idキャッシュのCANONICALIZATION_RULE_VERSION/ID_INDEX_SCHEMA_VERSIONとはスコープを分離し、いずれとも一緒に読み書きしない。
config.tomlを正本とするchanges compute <from> <to>は、from・to双方の[knowledge].schema_versionをそれぞれのref自身がコミットしているconfig.tomlから(git ls-tree+git cat-file経由で、ワーキングツリーではなく)解決する。実行中のCLIバージョンからは推定せず、milestone.ymlからも読まない。これにより、マイルストーンタグ・任意コミット・将来のPR base/head比較のいずれにも同じ解決方法が一律に使える。
milestone.ymlは監査用の複写を持つが、正本にはしないmilestone initはcommit_oidとknowledge_schema_versionを追加で解決・記録する。
id: v2
commit_oid: 0123456789abcdef...
knowledge_schema_version: 1
これらはtagから解決した値の監査・表示用複写であり、changes computeはこれを参照しない。milestone initは本変更後も冪等なままであり、既存のmilestone.ymlは変更前と同様に不変とする。
changes computeとbackfill runは比較前にバージョンを解決する両者とも同じchanges::compute_changes_with_warningsを経由する(application::compute_changesとbackfill_run_with_policyがそれぞれ直接呼ぶ。ChangeAnalyzer::compute/compute_changes/compute_changes_between_refsは、ChangeEventだけを必要とする呼び出し元向けの薄いラッパー)。そのためバージョンゲートは1箇所に実装するだけで全ての呼び出し元に自動的に適用される。
fromとtoが異なる既知バージョンを報告した場合、またはいずれかがこのCLIビルドが知らない未来のバージョンを報告した場合、compute_changes_between_refsは何も計算する前にErrorKind::Unsupportedを持つio::Errorを返す。ChangeEventは生成されず、呼び出し元(application::compute_changes)はreplace_fileによる書き込みに到達しないため、既存のchanges/<to>.yamlは変更されずに残る。これは新しいJSON専用エラーチャンネルを追加するのではなく、既存のerror: {err} → stderr → exit 1 の経路(src/main.rs)をそのまま再利用する。
backfill runは同じUnsupportedエラーを、run全体を中断する理由ではなく、そのペアだけのスキップとして扱う。該当ペアはBackfillReport.incompatibleに記録され、git notesには記録されない。そのため後続のrunは自動的に再試行し、converterが実装された時点で手動の再実行操作なしに成功するようになる。
.markharness/knowledge/は本機能導入以前の全ての未変更refにおいてバージョン情報を持たない。そのためconfig.tomlに[knowledge]テーブルがない(あるいはconfig.toml自体が存在しない)refは、legacyスキーマバージョン1とみなし、changes computeはこの推定を無言で行わずwarningとして表示する。このwarningは構造化フィールド(CommandOutcome::ChangesComputed.warnings: Vec<String>)であり、HumanPresenter(warning: ...行)とJsonPresenter(既存JSON envelope内の"warnings"配列。ただし何も警告がない場合は[]ではなくキー自体を省略する — §9参照)の両方でレンダリングされる。
milestone.ymlの監査コピーは書き込むだけでなく検証もするchanges compute・backfill runはいずれも、単一の共有関数compute_changes_with_warningsを通じて各refのKnowledgeスキーマバージョンを解決する。この関数は各refのバージョンを解決した直後(§8参照。監査検証はその解決結果をそのまま再利用し、再解決はしない)、各ref自身の.markharness/executions/<name>/milestone.yml(その名前のものが存在する場合)を、その名前が指すはずのtagと突き合わせて検証する — 記録されたcommit_oidとknowledge_schema_versionが、そのtagが今実際に解決する値と一致していなければならない(バージョン解決ポリシー表の「milestone.yml とtag内の正本が不一致 |
エラーとして報告する」行。従来は未実装だった)。これらのフィールドを持たないmilestone.yml(本ADR以前のもの)は検証対象外とし、表の次の行の通りtagの正本のみを信頼する。不一致(tagの移動、または手編集)はfail closedのUnsupportedスキップではなく、ハードなInvalidDataエラーとする。そのためbackfill runは、未対応スキーマバージョンのペアのように黙ってスキップし後で再試行する、という扱いをしない — 古い・改ざんされた監査コピーはconverterではなく人間の確認を必要とするため。 |
compute_changes_with_warningsは各refのKnowledgeスキーマバージョンをちょうど一度だけ解決し、その同じResolvedSchemaVersionを3つの利用箇所すべて — §7のmilestone.yml監査検証、fail-closedゲート、legacy warningのテキスト — で再利用する。それぞれが独立に再解決することはしない(Standardsレビューでの指摘: 最初はapplication::compute_changesとchanges::compute_changesの間で、次にmilestone::verify_audit_matches_tagとその呼び出し元の間で、同じ重複が2回検出された。Git読み取りの重複に加え、2回の解決結果が(例えばtag更新と競合した場合などに)食い違えば、3つのうち2つが一致しなくなるリスクが実在した)。そのためverify_audit_matches_tagは自ら解決するのではなく、呼び出し元が既に解決したResolvedSchemaVersionを引数として受け取る。
warningsはoptionalなJSONフィールドとし、backfill runもchanges computeと同じ情報を報告するchanges compute --jsonの出力では、報告すべき警告が何もない場合"warnings"キー自体を省略し、"warnings":[]としては出力しない — 設計ドキュメントのJSON contract規約(§5)は、同一schema_version内での追加をoptionalなフィールドに限って許可しており、常に存在するフィールド(空配列であっても)は実質的にrequiredなフィールドであり、既存の全利用者に対してv1 contractの形を変えてしまう。
backfill runも、changes computeと同じlegacy schema versionのwarningを収集する(ペアごとにcompute_changes_with_warningsを1回呼ぶ)。backfill runには現状--jsonモードがないため、warning: ...行として出力し、非互換としてスキップした各ペアの名前も列挙する — fail-closedゲートだけでなくchanges computeと同じポリシーに従う。バージョン非互換なペアは、run自体は成功していても実際には未処理の作業を残すため、BackfillReport.incompatibleが空でない場合backfill runは終了コード0ではなく1で終了する。見落とされかねない「クリーンな成功」を報告しないようにするためである。
BackfillReport.incompatibleは名前だけでなくIncompatiblePair { to_milestone, reason }を保持する — reasonはfail-closedゲート自身のio::Errorメッセージそのもの(Specレビューでの指摘: 以前のバージョンはUnsupportedエラーを捕捉した上で捨て、代わりに汎用的な「非互換」の1行だけを出力していた)。issue #29 §5は、fail-closedの診断が両側のバージョンを名指しし、CLI更新またはmigrationが必要であることを述べることを要求している。backfill runはスキップした各ペアについてこの同じメッセージを出力し、利用者が理由を知るために手動でchanges computeを再実行する必要がないようにする。compute_changes_with_warningsは、該当するlegacy fallbackのwarningテキストも、その同じUnsupportedエラーのメッセージに折り込んでから返す(§6のwarningはComputeChangesOutcomeのOkパスにしか存在しないため、そうしないとゲートに失敗したペアはそのコンテキストを完全に失ってしまう)。
本プロジェクトはCargo.tomlのversionが0.xのプロトタイプ段階であり、release-and-license instructionsは「0.xバージョンはマイナー間で互換性を破ってよい」「1.0の基準はPROJECT.mdで管理し、現在の基準はUC8インポータの実装とスキーマの安定化」と定めている。つまりKnowledgeスキーマ自体が1.0に向けて安定化する前提のものであり、プロトタイプ期の設計検討のたびに[knowledge].schema_versionを刻むと、正式リリース後に本ADRの履歴を読んだ開発者が「どのバージョン境界が実際に議論を経た破壊的変更で、どれが単なる試行錯誤か」を区別できなくなる。
そのため、[knowledge].schema_versionはスキーマが安定した後(PROJECT.mdが定める1.0の基準を満たした後)に初めて本ADRの契約(§1〜10)が適用され始める運用とし、プロトタイプ期の破壊的変更(Behavior/Condition/ExpectedResultのフィールド追加・改名を含む)では上げない。将来、実データが存在する状態で1.0後にスキーマ破壊的変更が入る場合は、通常通り本ADRの契約に従いバージョンを上げる。この判断は、実データが存在せず旧形式との誤比較リスクが今この時点では発生しないという事実(schema変更を行う各ADR自身が個別に確認する)と併せて適用される。
issue #29が明示した対象外と一致する: 異なるスキーマ間のconverter、schema-only migrationを除外する意味的差分(semantic diff)、semantic hashおよびcanonicalization rule versionの変更、--allow-raw-schema-diffのようなescape hatch、既存Knowledgeを新スキーマへ書き換えるmigrationコマンド。本ADRはバージョンを解決可能にし、未対応の比較を無言で行わず安全に停止させることのみを行う。
src/knowledge_schema.rs(新規): resolve(ref → ResolvedSchemaVersion { version, is_legacy }。記録された値が不正(非整数、u32範囲外、[knowledge]自体がテーブルでない)な場合は「未記録」と黙って同一視せずハードエラーにする)、ensure_compatible(fail closedのゲート。相違・未来バージョンに加えversion 0も拒否)、legacy_warning、CURRENT_KNOWLEDGE_SCHEMA_VERSION。src/git.rs: milestone.ymlの監査用commit_oidのためにresolve_commit_oidを追加。src/milestone.rs: milestone_initがidに加えてcommit_oidとknowledge_schema_versionを書き込む。冪等性の挙動は変更なし。milestone.ymlの記録値をtagの現在の解決結果と突き合わせるverify_audit_matches_tagを追加。src/changes.rs: compute_changes_with_warningsを追加(各refのスキーマバージョンを一度だけ解決し、milestone::verify_audit_matches_tagとknowledge_schema::ensure_compatibleを実行した上でChangeEventとlegacy warningの両方を返す。fail closedでErrとなった場合も、該当するlegacy warningのテキストをその同じエラーのメッセージに折り込んでから返し、破棄しない)。compute_changes/compute_changes_between_refsはこれを呼ぶ薄いラッパーとなり、全呼び出し元が単一の解決経路を共有する。src/backfill.rs: BackfillReportにincompatible: Vec<IncompatiblePair>({ to_milestone, reason }。reasonはfail-closedゲートのエラーメッセージそのもの)とwarnings: Vec<String>を追加。backfill_run_with_policyはcompute_changes_with_warningsを呼び、ErrorKind::Unsupportedのペアをrunの中断ではなくスキップとして扱い(エラーテキストは破棄せず保持する)、warningを収集する。src/application.rs / src/presentation.rs: CommandOutcome::ChangesComputedにwarnings: Vec<String>を追加し、両Presenterでレンダリングする。JsonPresenterは空の場合"warnings":[]ではなくキー自体を省略する。src/cli.rs: backfill runは収集したwarningと非互換ペアの実際の理由を出力し、1件でもスキップがあれば終了コード1で終了する。src/init.rs: markharness initが既存のトップレベルschema_version = 1に加えて[knowledge]\nschema_version = 1を書き込む。