knowledge removeAccepted(2026-09-27決定、2026-09-27実装完了)。knowledge removeサブコマンド本体、identity::recovery/feature_ops::roll_forwardのクラッシュ耐性のある削除拡張、generateが追加実装なしで既に対応することを示す回帰テスト、およびパストラバーサル/symlink祖先に対する回帰テストをすべて満たした。詳細はchecklist-knowledge-remove.mdを参照。
0027§4は削除を意図的に先送りした。knowledge reconcileは削除を伴わないmergeのみをサポートし、exactモードや--delete/--allow-retireフラグは「誤ったスコープやAIの見落としが破壊的な結果を招く」ことと「初期版には、そのリスクを正当化する具体的な削除要件がない」ことを理由に未実装のまま残された。
その具体的要件が今、存在する。作者は誤って作成した、または不要になったRequirement・Feature・Behavior・Scenarioを削除する必要がある(そして0017§3が1 Scenario = 1 TestCaseと定めているため、これはTestCaseを削除する手段でもある)。基盤となる意味論は0021がすでに決着させている——「FeatureまたはTestCaseがknowledge/から削除されると、それは単にKnowledgeツリーから消える。UID再利用の禁止や専用のretired状態遷移は追跡しない」——ため、本ADRはretire/tombstoneモデルを新たに考案する必要はなく、削除をどう起動し、どの範囲に及ぼし、どうクラッシュ安全にするかだけを決める。
Knowledgeツリーには削除時の振る舞いが異なる2種類の参照形状がある。
Feature.requirement_uidsとScenario.requirement_uids(0031) — は値コレクション(0027§5)であり、削除されたUIDを単に省略できる。Behavior.featureとScenario.behavior(src/knowledge.rs) — は必須のStringフィールドで、「親なし」を表現できる状態が存在しない。親の削除がこれらを未解決のまま残すことは許されない。RequirementとFeatureのidはプロジェクト全体で一意(.markharness/knowledge/requirements/<id>/、.markharness/knowledge/features/<id>/)だが、BehaviorとScenarioのidは親の配下でのみ一意(features/<feature_id>/<behavior_id>/、features/<feature_id>/<behavior_id>/<scenario_id>/)であり、異なる親の下に同じslugが正当に存在し得る。
既存の複数ファイルにわたるKnowledge変更(0027)は作業ツリーへ直接書き込まれない。identity::recoveryのステージングプロトコル(begin_batch_with_payload → commit_batch → feature_ops::roll_forward → finish)を経由するため、1つのファイルを書いてから別のファイルを書くまでの間でクラッシュしても、変更前または変更後の状態のどちらかに必ず収束し、混在状態にはならない。identity::recovery::PendingKnowledgeFileは現在「このファイルの全内容を書く」ことしか表現できず、「このファイルはもう存在しない」ことを表現する手段がない。
markharness knowledge remove <type> <key>を独立したサブコマンドとして追加するmarkharness knowledge remove <requirement|feature|behavior|scenario> <key>
[--feature <id>] [--behavior <id>] [--dir <path>] [--json]
これはReconcileと並ぶ新規KnowledgeCommand::Removeバリアント(src/cli.rs)であり、reconcileのexact/--deleteモードではない。単一要素をID指定で明示的に削除する操作は、ADR 0027§4が懸念した「大きな宣言的Intentのうち名前を挙げなかった要素すべてを暗黙に、省略ベースで退役させる」というリスクを共有しない。したがって別個の、より狭い操作として別のInterfaceを持つ。reconcileの「mergeのみ・削除なし」という契約は変更しない。
CLIの既存の非対話的な運用方針(0028§3)と、axes pruneの「明示フラグが必要(プロンプトなし)」という先例に合わせ、確認プロンプトは追加しない。操作は常に明示的に指定された単一要素に対するものなので、即時実行する。
<key>はまずslugとして解決し、必要な場合のみ曖昧性解消を行うrequirementとfeature: <key>は常にプロジェクト全体で一意な表示id。曖昧になることはない。behaviorとscenario: <key>は表示id。一致が1件だけならそれが対象。2件以上一致する場合(異なる親の下に同じslug)、コマンドは全一致の完全な親パスを列挙した診断で失敗し、何も削除せずに停止する。--feature <id>(behavior用。scenarioでは--behavior <id>も併せて必須)で検索対象を1つの親に絞るか、既知であれば<key>自体を(不変識別子である)uid(0013)として渡す。uidは常に使えるわけではない(identity migrate未実行のプロジェクトではBehavior/ScenarioのuidがNoneのことがある)。そのため、uidだけに頼らず常に使える第二の曖昧性解消手段として親パス形式(--feature/--behavior)を用意する。--feature/--behaviorは、その組み合わせが何の意味も持ち得ない場合、解決処理を始める前に即座に拒否する: requirement/featureに対するどちらのフラグ(すでにプロジェクト全体で一意)、behaviorに対する--behavior(Behavior自身のスコープ指定キーは--featureであり--behaviorではない)、そしてscenarioに対して--featureなしの--behavior単独。最後のケースが最も重要である: 対になっていない--behaviorを(拒否せず)黙って無視すると、<key>はスコープなしのslugとして解決されてしまい、<key>だけで一意に決まるどのScenarioでも削除してしまう——それは呼び出し側が指定したBehaviorの配下にあるとは限らない。解決には新規のディレクトリ走査を書かず、identity::knowledge_walk::find_by_id/find_by_uid(src/identity/knowledge_walk.rs)を再利用する。
requirement_uidsにそのUIDを含むすべてのFeature・Scenarioに連鎖するが、それらのFeature・Scenario自体を削除するのではなく、requirement_uidsを書き換えてそのUIDを除去する(値コレクションの置き換え、0027§5)。Feature・Scenario自体は残る。featureでそれを指すすべてのBehaviorを物理削除し、Behaviorの削除は、behaviorでそれを指すすべてのScenarioを物理削除する——Behavior.feature/Scenario.behaviorが必須フィールドで「親なし」を表現する値を持たないため、これは再帰的に行われる。したがってRequirement・Feature・Behaviorの削除は、それを根とする部分木全体を常に取り除く: Requirement → Feature/Behavior/Scenario、Feature → Behavior/Scenario、Behavior → Scenario。knowledge removeは.markharness/knowledge/配下のファイルのみを削除する。以下には触れない。
generated/testcases/ — §5を参照。.markharness/case-definitions/(追記のみの履行記録、0017)、実行バインディング、削除されたScenarioのCase UIDを参照している可能性のあるrelease-scope選択。これらはダングリング参照になる。その検出や警告は本ADRの範囲外として明示的に扱う。具体的な必要性が生じた場合は将来のADRで対応する可能性があり、これは0021がidentityモデル全般に対して既に取ったYAGNIの立場と一致する。generateには新たなプルーニングロジックは不要generate::load_knowledge_snapshotは.markharness/knowledge/を呼び出しごとに新たに走査し、見つけたScenarioファイルごとに1つのTestCaseを構築する。application::generate_testcasesは、その都度そのスナップショットからgenerated/ディレクトリ全体を毎回置き換える(0017§3: 1 Scenario = 1 TestCase)。このため、knowledge removeによって(直接または連鎖により)削除されたScenarioは、追加のプルーニングコードなしに、次のgenerate実行の出力から既に欠落する。本ADRは、CLAUDE.mdのYAGNI原則に従い、その既存動作を証明する回帰テストのみを追加し、新規プロダクションコードは追加しない。
identity::recoveryのステージングプロトコルを削除を表現できるよう拡張するPendingKnowledgeFileと並ぶPendingKnowledgeDelete { relative_path: String }を追加し、新規IntentPayload::KnowledgeRemove { deletes: Vec<PendingKnowledgeDelete>, files: Vec<PendingKnowledgeFile> }バリアント(identity::recovery)を追加する——deletesは連鎖削除される要素のファイル/ディレクトリ、filesは逆参照の書き換えを表し、既存のIntentPayload::KnowledgeReconcile { files, moves }の形に倣う。
0021§4により、削除はidentity eventを発行しない(UID発行とrenameのみが追跡対象)。したがってknowledge removeは、内容のみのreconcileパッチと同様に、batch_eventsを空にしてbegin_batch_with_payload(root, Vec::new(), Some(payload))を呼ぶ。その論理的なコミットポイントはeventファイルではなく、commit_batchが書くcommit_marker_pathである。
run_startup_recoveryのすべての呼び出し元が既に使っている単一のディスパッチャであるidentity::feature_ops::roll_forwardを、IntentPayload::KnowledgeRemove用のmatchアームで拡張する。このアームはdeletesの各パスを冪等に削除し(既に削除済みでもエラーとしない。既存のmove再生が同じ理由で持つ前例と同様)、filesの各エントリをreplace_fileする。これは既存のすべての呼び出し箇所がrun_startup_recoveryに渡している、まさに同じディスパッチャなので、新規knowledge_removeモジュール自身以外のどの呼び出し箇所も変更する必要はない。
これにより、knowledge removeの連鎖削除+逆参照書き換えは、knowledge reconcileの複数ファイル書き込みと全く同様にクラッシュ耐性を持つ: あるファイルを削除してから別のファイルを書き換えるまでの間にクラッシュしても、次回起動時のリカバリスキャンで必ず「完全に適用された状態」に収束し、部分的に適用された状態にはならない。
knowledge removeは必須の親参照を未解決のまま残さない。knowledge remove操作(およびそれが必要としたクラッシュリカバリ)が完了した時点で、requirement_uidsはもはや存在しないRequirementのUIDを指すことはない。knowledge removeはidentity eventを一切発行しない(0021§4は変更されない)。.markharness/case-definitions/、実行バインディング、release-scope選択は、knowledge removeによって書き換えられることも削除されることもない。src/identity/recovery.rs: 新規PendingKnowledgeDelete型とIntentPayload::KnowledgeRemoveバリアント。src/identity/feature_ops.rs: 上記バリアント用のroll_forwardへの新規matchアーム。src/knowledge_remove.rs(またはsrc/knowledge_reconcile/remove.rs): 解決(slug/uid/親パス、曖昧性診断)、連鎖の計算、逆参照のデタッチ、およびlock/recovery/commitの手順を、knowledge_reconcile::execute::reconcile_creationの既存の形に沿って実装する。src/cli.rs: 新規KnowledgeCommand::Removeバリアントとdispatchアーム。generate.rs/application.rsのテストスイートに、削除済みScenarioのプルーニングが既に機能することを証明する回帰テストを追加する(そこにプロダクションコードの変更はない)。docs/knowledge-from-code.ai.md)に、knowledge reconcileと並ぶ標準経路のコマンドとしてknowledge removeを追加する。reconcile --delete/exactモードとして実装する:不採用。exactモードの危険性は、大きな宣言的Intentで名前を挙げなかったものすべてを暗黙に、省略ベースで退役させることに特有のものであり、単一要素をID指定で明示的に削除する操作はその失敗モードを共有せず、exactが必要とするであろう同等の権限確認の儀式も必要ない。別個の、より狭いコマンドの方が、推論もテストもしやすい。uidを必須とし、なければエラーとする: 不採用。identity migrate未実行のプロジェクトで削除が不可能になってしまい、常に使える親パス(--feature/--behavior)という代替に対して安全性上の利点がない。knowledge reconcileが既にクラッシュ耐性のある操作として扱っている複数ファイル変更と同じ形であり、構造的に同一の失敗モードに対してここだけ小さな保証で済ませることは、明確な利点なしに一貫性を欠く。--force/確認フラグを追加する: 不採用。コマンドは既に明示的に指定された単一要素に範囲を限定されているため、確認ステップが意味のある形で防ぐべき「省略による大きな爆発範囲」というリスクがなく、CLIには合わせるべき他の対話的確認の先例もない。